From e27a8434e13fa4345626612ed38232adafc6ea1e Mon Sep 17 00:00:00 2001 From: dahoud Date: Sat, 29 Nov 2025 02:57:00 +0000 Subject: [PATCH] Refactroring --- .../unionflow/server/entity/Adhesion.java | 132 ++++ .../unionflow/server/entity/AuditLog.java | 81 ++ .../server/entity/TypeOrganisationEntity.java | 73 ++ .../server/repository/AdhesionRepository.java | 102 +++ .../server/repository/AuditLogRepository.java | 26 + .../TypeOrganisationRepository.java | 43 ++ .../server/resource/AdhesionResource.java | 697 ++++++++++++++++++ .../server/resource/AuditResource.java | 110 +++ .../server/resource/ExportResource.java | 115 +++ .../server/resource/MembreResource.java | 2 + .../server/resource/NotificationResource.java | 139 ++++ .../server/resource/OrganisationResource.java | 1 + .../server/resource/PreferencesResource.java | 75 ++ .../resource/TypeOrganisationResource.java | 162 ++++ .../server/resource/WaveResource.java | 213 ++++++ .../server/service/AdhesionService.java | 559 ++++++++++++++ .../server/service/AuditService.java | 229 ++++++ .../server/service/ExportService.java | 237 ++++++ .../service/TypeOrganisationService.java | 146 ++++ .../unionflow/server/service/WaveService.java | 281 +++++++ .../src/main/resources/application.properties | 7 + 21 files changed, 3430 insertions(+) create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Adhesion.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/AuditLog.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/TypeOrganisationEntity.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AdhesionRepository.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AuditLogRepository.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/TypeOrganisationRepository.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AdhesionResource.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AuditResource.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/ExportResource.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/NotificationResource.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/PreferencesResource.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/TypeOrganisationResource.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AdhesionService.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AuditService.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/ExportService.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/TypeOrganisationService.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/WaveService.java diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Adhesion.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Adhesion.java new file mode 100644 index 0000000..e5fbd8a --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Adhesion.java @@ -0,0 +1,132 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Adhesion avec UUID + * Représente une demande d'adhésion d'un membre à une organisation + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@Entity +@Table( + name = "adhesions", + indexes = { + @Index(name = "idx_adhesion_membre", columnList = "membre_id"), + @Index(name = "idx_adhesion_organisation", columnList = "organisation_id"), + @Index(name = "idx_adhesion_reference", columnList = "numero_reference", unique = true), + @Index(name = "idx_adhesion_statut", columnList = "statut"), + @Index(name = "idx_adhesion_date_demande", columnList = "date_demande") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Adhesion extends BaseEntity { + + @NotBlank + @Column(name = "numero_reference", unique = true, nullable = false, length = 50) + private String numeroReference; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id", nullable = false) + private Membre membre; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id", nullable = false) + private Organisation organisation; + + @NotNull + @Column(name = "date_demande", nullable = false) + private LocalDate dateDemande; + + @NotNull + @DecimalMin(value = "0.0", message = "Le montant des frais d'adhésion doit être positif") + @Digits(integer = 10, fraction = 2) + @Column(name = "frais_adhesion", nullable = false, precision = 12, scale = 2) + private BigDecimal fraisAdhesion; + + @Builder.Default + @DecimalMin(value = "0.0", message = "Le montant payé doit être positif") + @Digits(integer = 10, fraction = 2) + @Column(name = "montant_paye", nullable = false, precision = 12, scale = 2) + private BigDecimal montantPaye = BigDecimal.ZERO; + + @NotBlank + @Pattern(regexp = "^[A-Z]{3}$", message = "Le code devise doit être un code ISO à 3 lettres") + @Column(name = "code_devise", nullable = false, length = 3) + private String codeDevise; + + @NotBlank + @Pattern( + regexp = "^(EN_ATTENTE|APPROUVEE|REJETEE|ANNULEE|EN_PAIEMENT|PAYEE)$", + message = "Statut invalide") + @Column(name = "statut", nullable = false, length = 30) + private String statut; + + @Column(name = "date_approbation") + private LocalDate dateApprobation; + + @Column(name = "date_paiement") + private LocalDateTime datePaiement; + + @Size(max = 20) + @Column(name = "methode_paiement", length = 20) + private String methodePaiement; + + @Size(max = 100) + @Column(name = "reference_paiement", length = 100) + private String referencePaiement; + + @Size(max = 1000) + @Column(name = "motif_rejet", length = 1000) + private String motifRejet; + + @Size(max = 1000) + @Column(name = "observations", length = 1000) + private String observations; + + @Column(name = "approuve_par", length = 255) + private String approuvePar; + + @Column(name = "date_validation") + private LocalDate dateValidation; + + /** Méthode métier pour vérifier si l'adhésion est payée intégralement */ + public boolean isPayeeIntegralement() { + return montantPaye != null + && fraisAdhesion != null + && montantPaye.compareTo(fraisAdhesion) >= 0; + } + + /** Méthode métier pour vérifier si l'adhésion est en attente de paiement */ + public boolean isEnAttentePaiement() { + return "APPROUVEE".equals(statut) && !isPayeeIntegralement(); + } + + /** Méthode métier pour calculer le montant restant à payer */ + public BigDecimal getMontantRestant() { + if (fraisAdhesion == null) return BigDecimal.ZERO; + if (montantPaye == null) return fraisAdhesion; + BigDecimal restant = fraisAdhesion.subtract(montantPaye); + return restant.compareTo(BigDecimal.ZERO) > 0 ? restant : BigDecimal.ZERO; + } +} + + + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/AuditLog.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/AuditLog.java new file mode 100644 index 0000000..6ec2bee --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/AuditLog.java @@ -0,0 +1,81 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; + +/** + * Entité pour les logs d'audit + * Enregistre toutes les actions importantes du système + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@Entity +@Table(name = "audit_logs", indexes = { + @Index(name = "idx_audit_date_heure", columnList = "date_heure"), + @Index(name = "idx_audit_utilisateur", columnList = "utilisateur"), + @Index(name = "idx_audit_module", columnList = "module"), + @Index(name = "idx_audit_type_action", columnList = "type_action"), + @Index(name = "idx_audit_severite", columnList = "severite") +}) +@Getter +@Setter +public class AuditLog extends BaseEntity { + + @Column(name = "type_action", nullable = false, length = 50) + private String typeAction; + + @Column(name = "severite", nullable = false, length = 20) + private String severite; + + @Column(name = "utilisateur", length = 255) + private String utilisateur; + + @Column(name = "role", length = 50) + private String role; + + @Column(name = "module", length = 50) + private String module; + + @Column(name = "description", length = 500) + private String description; + + @Column(name = "details", columnDefinition = "TEXT") + private String details; + + @Column(name = "ip_address", length = 45) + private String ipAddress; + + @Column(name = "user_agent", length = 500) + private String userAgent; + + @Column(name = "session_id", length = 255) + private String sessionId; + + @Column(name = "date_heure", nullable = false) + private LocalDateTime dateHeure; + + @Column(name = "donnees_avant", columnDefinition = "TEXT") + private String donneesAvant; + + @Column(name = "donnees_apres", columnDefinition = "TEXT") + private String donneesApres; + + @Column(name = "entite_id", length = 255) + private String entiteId; + + @Column(name = "entite_type", length = 100) + private String entiteType; + + @PrePersist + protected void onCreate() { + if (dateHeure == null) { + dateHeure = LocalDateTime.now(); + } + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/TypeOrganisationEntity.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/TypeOrganisationEntity.java new file mode 100644 index 0000000..988a9f4 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/TypeOrganisationEntity.java @@ -0,0 +1,73 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +/** + * Entité persistée représentant un type d'organisation. + * + *

Cette entité permet de gérer dynamiquement le catalogue des types d'organisations + * (codes, libellés, description, ordre d'affichage, activation/désactivation). + * + *

Le champ {@code code} doit rester synchronisé avec l'enum {@link + * dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation} pour les types + * standards fournis par la plateforme. + */ +@Entity +@Table( + name = "uf_type_organisation", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_type_organisation_code", + columnNames = {"code"}) + }) +public class TypeOrganisationEntity extends BaseEntity { + + @Column(name = "code", length = 50, nullable = false, unique = true) + private String code; + + @Column(name = "libelle", length = 150, nullable = false) + private String libelle; + + @Column(name = "description", length = 500) + private String description; + + @Column(name = "ordre_affichage") + private Integer ordreAffichage; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getLibelle() { + return libelle; + } + + public void setLibelle(String libelle) { + this.libelle = libelle; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getOrdreAffichage() { + return ordreAffichage; + } + + public void setOrdreAffichage(Integer ordreAffichage) { + this.ordreAffichage = ordreAffichage; + } +} + + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AdhesionRepository.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AdhesionRepository.java new file mode 100644 index 0000000..0929487 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AdhesionRepository.java @@ -0,0 +1,102 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.Adhesion; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Adhesion + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@ApplicationScoped +public class AdhesionRepository extends BaseRepository { + + public AdhesionRepository() { + super(Adhesion.class); + } + + /** + * Trouve une adhésion par son numéro de référence + * + * @param numeroReference numéro de référence unique + * @return Optional contenant l'adhésion si trouvée + */ + public Optional findByNumeroReference(String numeroReference) { + TypedQuery query = + entityManager.createQuery( + "SELECT a FROM Adhesion a WHERE a.numeroReference = :numeroReference", Adhesion.class); + query.setParameter("numeroReference", numeroReference); + return query.getResultStream().findFirst(); + } + + /** + * Trouve toutes les adhésions d'un membre + * + * @param membreId identifiant du membre + * @return liste des adhésions du membre + */ + public List findByMembreId(UUID membreId) { + TypedQuery query = + entityManager.createQuery( + "SELECT a FROM Adhesion a WHERE a.membre.id = :membreId", Adhesion.class); + query.setParameter("membreId", membreId); + return query.getResultList(); + } + + /** + * Trouve toutes les adhésions d'une organisation + * + * @param organisationId identifiant de l'organisation + * @return liste des adhésions de l'organisation + */ + public List findByOrganisationId(UUID organisationId) { + TypedQuery query = + entityManager.createQuery( + "SELECT a FROM Adhesion a WHERE a.organisation.id = :organisationId", Adhesion.class); + query.setParameter("organisationId", organisationId); + return query.getResultList(); + } + + /** + * Trouve toutes les adhésions par statut + * + * @param statut statut de l'adhésion + * @return liste des adhésions avec le statut spécifié + */ + public List findByStatut(String statut) { + TypedQuery query = + entityManager.createQuery("SELECT a FROM Adhesion a WHERE a.statut = :statut", Adhesion.class); + query.setParameter("statut", statut); + return query.getResultList(); + } + + /** + * Trouve toutes les adhésions en attente + * + * @return liste des adhésions en attente + */ + public List findEnAttente() { + return findByStatut("EN_ATTENTE"); + } + + /** + * Trouve toutes les adhésions approuvées en attente de paiement + * + * @return liste des adhésions approuvées non payées + */ + public List findApprouveesEnAttentePaiement() { + TypedQuery query = + entityManager.createQuery( + "SELECT a FROM Adhesion a WHERE a.statut = :statut AND (a.montantPaye IS NULL OR a.montantPaye < a.fraisAdhesion)", + Adhesion.class); + query.setParameter("statut", "APPROUVEE"); + return query.getResultList(); + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AuditLogRepository.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AuditLogRepository.java new file mode 100644 index 0000000..bd78702 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/AuditLogRepository.java @@ -0,0 +1,26 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.AuditLog; +import jakarta.enterprise.context.ApplicationScoped; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * Repository pour les logs d'audit + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@ApplicationScoped +public class AuditLogRepository extends BaseRepository { + + public AuditLogRepository() { + super(AuditLog.class); + } + + // Les méthodes de recherche spécifiques peuvent être ajoutées ici si nécessaire + // Pour l'instant, on utilise les méthodes de base et les requêtes dans le service +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/TypeOrganisationRepository.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/TypeOrganisationRepository.java new file mode 100644 index 0000000..9b842e9 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/repository/TypeOrganisationRepository.java @@ -0,0 +1,43 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.TypeOrganisationEntity; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; +import java.util.List; +import java.util.Optional; + +/** + * Repository pour l'entité {@link TypeOrganisationEntity}. + * + *

Permet de gérer le catalogue des types d'organisations. + */ +@ApplicationScoped +public class TypeOrganisationRepository extends BaseRepository { + + public TypeOrganisationRepository() { + super(TypeOrganisationEntity.class); + } + + /** Recherche un type par son code fonctionnel. */ + public Optional findByCode(String code) { + TypedQuery query = + entityManager.createQuery( + "SELECT t FROM TypeOrganisationEntity t WHERE UPPER(t.code) = UPPER(:code)", + TypeOrganisationEntity.class); + query.setParameter("code", code); + return query.getResultStream().findFirst(); + } + + /** Liste les types actifs, triés par ordreAffichage puis libellé. */ + public List listActifsOrdennes() { + return entityManager + .createQuery( + "SELECT t FROM TypeOrganisationEntity t " + + "WHERE t.actif = true " + + "ORDER BY COALESCE(t.ordreAffichage, 9999), t.libelle", + TypeOrganisationEntity.class) + .getResultList(); + } +} + + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AdhesionResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AdhesionResource.java new file mode 100644 index 0000000..d05452f --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AdhesionResource.java @@ -0,0 +1,697 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.finance.AdhesionDTO; +import dev.lions.unionflow.server.service.AdhesionService; +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 java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +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; + +/** + * Resource REST pour la gestion des adhésions + * Expose les endpoints API pour les opérations CRUD sur les adhésions + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@Path("/api/adhesions") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Adhésions", description = "Gestion des demandes d'adhésion des membres") +@Slf4j +public class AdhesionResource { + + @Inject AdhesionService adhesionService; + + /** Récupère toutes les adhésions avec pagination */ + @GET + @Operation( + summary = "Lister toutes les adhésions", + description = "Récupère la liste paginée de toutes les adhésions") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Liste des adhésions récupérée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = AdhesionDTO.class))), + @APIResponse(responseCode = "400", description = "Paramètres de pagination invalides"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAllAdhesions( + @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.info("GET /api/adhesions - page: {}, size: {}", page, size); + + List adhesions = adhesionService.getAllAdhesions(page, size); + + log.info("Récupération réussie de {} adhésions", adhesions.size()); + return Response.ok(adhesions).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des adhésions", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération des adhésions", "message", e.getMessage())) + .build(); + } + } + + /** Récupère une adhésion par son ID */ + @GET + @Path("/{id}") + @Operation( + summary = "Récupérer une adhésion par ID", + description = "Récupère les détails d'une adhésion spécifique") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Adhésion trouvée", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = AdhesionDTO.class))), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionById( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id) { + + try { + log.info("GET /api/adhesions/{}", id); + + AdhesionDTO adhesion = adhesionService.getAdhesionById(id); + + log.info("Adhésion récupérée avec succès - ID: {}", id); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Récupère une adhésion par son numéro de référence */ + @GET + @Path("/reference/{numeroReference}") + @Operation( + summary = "Récupérer une adhésion par référence", + description = "Récupère une adhésion par son numéro de référence unique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Adhésion trouvée"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionByReference( + @Parameter(description = "Numéro de référence de l'adhésion", required = true) + @PathParam("numeroReference") + @NotNull + String numeroReference) { + + try { + log.info("GET /api/adhesions/reference/{}", numeroReference); + + AdhesionDTO adhesion = adhesionService.getAdhesionByReference(numeroReference); + + log.info("Adhésion récupérée avec succès - Référence: {}", numeroReference); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée - Référence: {}", numeroReference); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "reference", numeroReference)) + .build(); + } catch (Exception e) { + log.error( + "Erreur lors de la récupération de l'adhésion - Référence: " + numeroReference, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Crée une nouvelle adhésion */ + @POST + @Operation( + summary = "Créer une nouvelle adhésion", + description = "Crée une nouvelle demande d'adhésion pour un membre") + @APIResponses({ + @APIResponse( + responseCode = "201", + description = "Adhésion créée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = AdhesionDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Membre ou organisation non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response createAdhesion( + @Parameter(description = "Données de l'adhésion à créer", required = true) @Valid + AdhesionDTO adhesionDTO) { + + try { + log.info( + "POST /api/adhesions - Création adhésion pour membre: {} et organisation: {}", + adhesionDTO.getMembreId(), + adhesionDTO.getOrganisationId()); + + AdhesionDTO nouvelleAdhesion = adhesionService.createAdhesion(adhesionDTO); + + log.info( + "Adhésion créée avec succès - ID: {}, Référence: {}", + nouvelleAdhesion.getId(), + nouvelleAdhesion.getNumeroReference()); + + return Response.status(Response.Status.CREATED).entity(nouvelleAdhesion).build(); + + } catch (NotFoundException e) { + log.warn("Membre ou organisation non trouvé lors de la création d'adhésion"); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Membre ou organisation non trouvé", "message", e.getMessage())) + .build(); + } catch (IllegalArgumentException e) { + log.warn("Données invalides pour la création d'adhésion: {}", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création de l'adhésion", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la création de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Met à jour une adhésion existante */ + @PUT + @Path("/{id}") + @Operation( + summary = "Mettre à jour une adhésion", + description = "Met à jour les données d'une adhésion existante") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Adhésion mise à jour avec succès"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response updateAdhesion( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id, + @Parameter(description = "Nouvelles données de l'adhésion", required = true) @Valid + AdhesionDTO adhesionDTO) { + + try { + log.info("PUT /api/adhesions/{}", id); + + AdhesionDTO adhesionMiseAJour = adhesionService.updateAdhesion(id, adhesionDTO); + + log.info("Adhésion mise à jour avec succès - ID: {}", id); + return Response.ok(adhesionMiseAJour).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour mise à jour - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalArgumentException e) { + log.warn( + "Données invalides pour la mise à jour d'adhésion - ID: {}, Erreur: {}", id, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la mise à jour de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la mise à jour de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Supprime une adhésion */ + @DELETE + @Path("/{id}") + @Operation( + summary = "Supprimer une adhésion", + description = "Supprime (annule) une adhésion") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Adhésion supprimée avec succès"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse( + responseCode = "409", + description = "Impossible de supprimer une adhésion payée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response deleteAdhesion( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id) { + + try { + log.info("DELETE /api/adhesions/{}", id); + + adhesionService.deleteAdhesion(id); + + log.info("Adhésion supprimée avec succès - ID: {}", id); + return Response.noContent().build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour suppression - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalStateException e) { + log.warn("Impossible de supprimer l'adhésion - ID: {}, Raison: {}", id, e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(Map.of("error", "Impossible de supprimer l'adhésion", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la suppression de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la suppression de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Approuve une adhésion */ + @POST + @Path("/{id}/approuver") + @Operation( + summary = "Approuver une adhésion", + description = "Approuve une demande d'adhésion en attente") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Adhésion approuvée avec succès"), + @APIResponse(responseCode = "400", description = "L'adhésion ne peut pas être approuvée"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response approuverAdhesion( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id, + @Parameter(description = "Nom de l'utilisateur qui approuve") + @QueryParam("approuvePar") + String approuvePar) { + + try { + log.info("POST /api/adhesions/{}/approuver", id); + + AdhesionDTO adhesion = adhesionService.approuverAdhesion(id, approuvePar); + + log.info("Adhésion approuvée avec succès - ID: {}", id); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour approbation - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalStateException e) { + log.warn("Impossible d'approuver l'adhésion - ID: {}, Raison: {}", id, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Impossible d'approuver l'adhésion", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de l'approbation de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de l'approbation de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Rejette une adhésion */ + @POST + @Path("/{id}/rejeter") + @Operation( + summary = "Rejeter une adhésion", + description = "Rejette une demande d'adhésion en attente") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Adhésion rejetée avec succès"), + @APIResponse(responseCode = "400", description = "L'adhésion ne peut pas être rejetée"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response rejeterAdhesion( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id, + @Parameter(description = "Motif du rejet", required = true) @QueryParam("motifRejet") + @NotNull + String motifRejet) { + + try { + log.info("POST /api/adhesions/{}/rejeter", id); + + AdhesionDTO adhesion = adhesionService.rejeterAdhesion(id, motifRejet); + + log.info("Adhésion rejetée avec succès - ID: {}", id); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour rejet - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalStateException e) { + log.warn("Impossible de rejeter l'adhésion - ID: {}, Raison: {}", id, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Impossible de rejeter l'adhésion", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors du rejet de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors du rejet de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Enregistre un paiement pour une adhésion */ + @POST + @Path("/{id}/paiement") + @Operation( + summary = "Enregistrer un paiement", + description = "Enregistre un paiement pour une adhésion approuvée") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Paiement enregistré avec succès"), + @APIResponse(responseCode = "400", description = "L'adhésion ne peut pas recevoir de paiement"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response enregistrerPaiement( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id, + @Parameter(description = "Montant payé", required = true) @QueryParam("montantPaye") + @NotNull + BigDecimal montantPaye, + @Parameter(description = "Méthode de paiement") @QueryParam("methodePaiement") + String methodePaiement, + @Parameter(description = "Référence du paiement") @QueryParam("referencePaiement") + String referencePaiement) { + + try { + log.info("POST /api/adhesions/{}/paiement", id); + + AdhesionDTO adhesion = + adhesionService.enregistrerPaiement(id, montantPaye, methodePaiement, referencePaiement); + + log.info("Paiement enregistré avec succès pour l'adhésion - ID: {}", id); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour paiement - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalStateException e) { + log.warn("Impossible d'enregistrer le paiement - ID: {}, Raison: {}", id, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity( + Map.of("error", "Impossible d'enregistrer le paiement", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de l'enregistrement du paiement - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de l'enregistrement du paiement", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les adhésions d'un membre */ + @GET + @Path("/membre/{membreId}") + @Operation( + summary = "Lister les adhésions d'un membre", + description = "Récupère toutes les adhésions d'un membre spécifique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des adhésions du membre"), + @APIResponse(responseCode = "404", description = "Membre non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionsByMembre( + @Parameter(description = "Identifiant du membre", required = true) + @PathParam("membreId") + @NotNull + UUID membreId, + @Parameter(description = "Numéro de page", 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.info("GET /api/adhesions/membre/{} - page: {}, size: {}", membreId, page, size); + + List adhesions = adhesionService.getAdhesionsByMembre(membreId, page, size); + + log.info( + "Récupération réussie de {} adhésions pour le membre {}", adhesions.size(), membreId); + return Response.ok(adhesions).build(); + + } catch (NotFoundException e) { + log.warn("Membre non trouvé - ID: {}", membreId); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Membre non trouvé", "membreId", membreId)) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des adhésions du membre - ID: " + membreId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la récupération des adhésions", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les adhésions d'une organisation */ + @GET + @Path("/organisation/{organisationId}") + @Operation( + summary = "Lister les adhésions d'une organisation", + description = "Récupère toutes les adhésions d'une organisation spécifique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des adhésions de l'organisation"), + @APIResponse(responseCode = "404", description = "Organisation non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionsByOrganisation( + @Parameter(description = "Identifiant de l'organisation", required = true) + @PathParam("organisationId") + @NotNull + UUID organisationId, + @Parameter(description = "Numéro de page", 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.info( + "GET /api/adhesions/organisation/{} - page: {}, size: {}", organisationId, page, size); + + List adhesions = + adhesionService.getAdhesionsByOrganisation(organisationId, page, size); + + log.info( + "Récupération réussie de {} adhésions pour l'organisation {}", + adhesions.size(), + organisationId); + return Response.ok(adhesions).build(); + + } catch (NotFoundException e) { + log.warn("Organisation non trouvée - ID: {}", organisationId); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Organisation non trouvée", "organisationId", organisationId)) + .build(); + } catch (Exception e) { + log.error( + "Erreur lors de la récupération des adhésions de l'organisation - ID: " + organisationId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la récupération des adhésions", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les adhésions par statut */ + @GET + @Path("/statut/{statut}") + @Operation( + summary = "Lister les adhésions par statut", + description = "Récupère toutes les adhésions ayant un statut spécifique") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Liste des adhésions avec le statut spécifié"), + @APIResponse(responseCode = "400", description = "Statut invalide"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionsByStatut( + @Parameter(description = "Statut des adhésions", required = true, example = "EN_ATTENTE") + @PathParam("statut") + @NotNull + String statut, + @Parameter(description = "Numéro de page", 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.info("GET /api/adhesions/statut/{} - page: {}, size: {}", statut, page, size); + + List adhesions = adhesionService.getAdhesionsByStatut(statut, page, size); + + log.info("Récupération réussie de {} adhésions avec statut {}", adhesions.size(), statut); + return Response.ok(adhesions).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des adhésions par statut - Statut: " + statut, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la récupération des adhésions", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les adhésions en attente */ + @GET + @Path("/en-attente") + @Operation( + summary = "Lister les adhésions en attente", + description = "Récupère toutes les adhésions en attente d'approbation") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des adhésions en attente"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionsEnAttente( + @Parameter(description = "Numéro de page", 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.info("GET /api/adhesions/en-attente - page: {}, size: {}", page, size); + + List adhesions = adhesionService.getAdhesionsEnAttente(page, size); + + log.info("Récupération réussie de {} adhésions en attente", adhesions.size()); + return Response.ok(adhesions).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des adhésions en attente", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération des adhésions en attente", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les statistiques des adhésions */ + @GET + @Path("/stats") + @Operation( + summary = "Statistiques des adhésions", + description = "Récupère les statistiques globales des adhésions") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getStatistiquesAdhesions() { + try { + log.info("GET /api/adhesions/stats"); + + Map statistiques = adhesionService.getStatistiquesAdhesions(); + + log.info("Statistiques récupérées avec succès"); + return Response.ok(statistiques).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des statistiques", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération des statistiques", "message", e.getMessage())) + .build(); + } + } +} + + + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AuditResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AuditResource.java new file mode 100644 index 0000000..fe5b896 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/AuditResource.java @@ -0,0 +1,110 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.admin.AuditLogDTO; +import dev.lions.unionflow.server.service.AuditService; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * Resource REST pour la gestion des logs d'audit + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@Path("/api/audit") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Audit", description = "Gestion des logs d'audit") +@Slf4j +public class AuditResource { + + @Inject + AuditService auditService; + + @GET + @Operation(summary = "Liste tous les logs d'audit", description = "Récupère tous les logs avec pagination") + public Response listerTous( + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("50") int size, + @QueryParam("sortBy") @DefaultValue("dateHeure") String sortBy, + @QueryParam("sortOrder") @DefaultValue("desc") String sortOrder) { + + try { + Map result = auditService.listerTous(page, size, sortBy, sortOrder); + return Response.ok(result).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des logs d'audit", e); + return Response.serverError() + .entity(Map.of("error", "Erreur lors de la récupération des logs: " + e.getMessage())) + .build(); + } + } + + @POST + @Path("/rechercher") + @Operation(summary = "Recherche des logs avec filtres", description = "Recherche avancée avec filtres multiples") + public Response rechercher( + @QueryParam("dateDebut") String dateDebutStr, + @QueryParam("dateFin") String dateFinStr, + @QueryParam("typeAction") String typeAction, + @QueryParam("severite") String severite, + @QueryParam("utilisateur") String utilisateur, + @QueryParam("module") String module, + @QueryParam("ipAddress") String ipAddress, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("50") int size) { + + try { + LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; + LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; + + Map result = auditService.rechercher( + dateDebut, dateFin, typeAction, severite, utilisateur, module, ipAddress, page, size); + return Response.ok(result).build(); + } catch (Exception e) { + log.error("Erreur lors de la recherche des logs d'audit", e); + return Response.serverError() + .entity(Map.of("error", "Erreur lors de la recherche: " + e.getMessage())) + .build(); + } + } + + @POST + @Operation(summary = "Enregistre un nouveau log d'audit", description = "Crée une nouvelle entrée dans le journal d'audit") + public Response enregistrerLog(@Valid AuditLogDTO dto) { + try { + AuditLogDTO result = auditService.enregistrerLog(dto); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (Exception e) { + log.error("Erreur lors de l'enregistrement du log d'audit", e); + return Response.serverError() + .entity(Map.of("error", "Erreur lors de l'enregistrement: " + e.getMessage())) + .build(); + } + } + + @GET + @Path("/statistiques") + @Operation(summary = "Récupère les statistiques d'audit", description = "Retourne les statistiques globales des logs") + public Response getStatistiques() { + try { + Map stats = auditService.getStatistiques(); + return Response.ok(stats).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des statistiques", e); + return Response.serverError() + .entity(Map.of("error", "Erreur lors de la récupération des statistiques: " + e.getMessage())) + .build(); + } + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/ExportResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/ExportResource.java new file mode 100644 index 0000000..28161c0 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/ExportResource.java @@ -0,0 +1,115 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.service.ExportService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** Resource REST pour l'export des données */ +@Path("/api/export") +@ApplicationScoped +@Tag(name = "Export", description = "API d'export des données") +public class ExportResource { + + private static final Logger LOG = Logger.getLogger(ExportResource.class); + + @Inject ExportService exportService; + + @GET + @Path("/cotisations/csv") + @Produces("text/csv") + @Operation(summary = "Exporter les cotisations en CSV") + @APIResponse(responseCode = "200", description = "Fichier CSV généré") + public Response exporterCotisationsCSV( + @QueryParam("statut") String statut, + @QueryParam("type") String type, + @QueryParam("associationId") UUID associationId) { + LOG.info("Export CSV des cotisations"); + + byte[] csv = exportService.exporterToutesCotisationsCSV(statut, type, associationId); + + return Response.ok(csv) + .header("Content-Disposition", "attachment; filename=\"cotisations.csv\"") + .header("Content-Type", "text/csv; charset=UTF-8") + .build(); + } + + @POST + @Path("/cotisations/csv") + @Consumes(MediaType.APPLICATION_JSON) + @Produces("text/csv") + @Operation(summary = "Exporter des cotisations spécifiques en CSV") + @APIResponse(responseCode = "200", description = "Fichier CSV généré") + public Response exporterCotisationsSelectionneesCSV(List cotisationIds) { + LOG.infof("Export CSV de %d cotisations", cotisationIds.size()); + + byte[] csv = exportService.exporterCotisationsCSV(cotisationIds); + + return Response.ok(csv) + .header("Content-Disposition", "attachment; filename=\"cotisations.csv\"") + .header("Content-Type", "text/csv; charset=UTF-8") + .build(); + } + + @GET + @Path("/cotisations/{cotisationId}/recu") + @Produces("text/plain") + @Operation(summary = "Générer un reçu de paiement") + @APIResponse(responseCode = "200", description = "Reçu généré") + public Response genererRecu(@PathParam("cotisationId") UUID cotisationId) { + LOG.infof("Génération reçu pour: %s", cotisationId); + + byte[] recu = exportService.genererRecuPaiement(cotisationId); + + return Response.ok(recu) + .header("Content-Disposition", "attachment; filename=\"recu-" + cotisationId + ".txt\"") + .header("Content-Type", "text/plain; charset=UTF-8") + .build(); + } + + @POST + @Path("/cotisations/recus") + @Consumes(MediaType.APPLICATION_JSON) + @Produces("text/plain") + @Operation(summary = "Générer des reçus groupés") + @APIResponse(responseCode = "200", description = "Reçus générés") + public Response genererRecusGroupes(List cotisationIds) { + LOG.infof("Génération de %d reçus", cotisationIds.size()); + + byte[] recus = exportService.genererRecusGroupes(cotisationIds); + + return Response.ok(recus) + .header("Content-Disposition", "attachment; filename=\"recus-groupes.txt\"") + .header("Content-Type", "text/plain; charset=UTF-8") + .build(); + } + + @GET + @Path("/rapport/mensuel") + @Produces("text/plain") + @Operation(summary = "Générer un rapport mensuel") + @APIResponse(responseCode = "200", description = "Rapport généré") + public Response genererRapportMensuel( + @QueryParam("annee") int annee, + @QueryParam("mois") int mois, + @QueryParam("associationId") UUID associationId) { + LOG.infof("Génération rapport mensuel: %d/%d", mois, annee); + + byte[] rapport = exportService.genererRapportMensuel(annee, mois, associationId); + + return Response.ok(rapport) + .header("Content-Disposition", + "attachment; filename=\"rapport-" + annee + "-" + String.format("%02d", mois) + ".txt\"") + .header("Content-Type", "text/plain; charset=UTF-8") + .build(); + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java index 59d56ae..a9fded9 100644 --- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java @@ -7,6 +7,7 @@ import dev.lions.unionflow.server.entity.Membre; import dev.lions.unionflow.server.service.MembreService; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; +import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -90,6 +91,7 @@ public class MembreResource { } @POST + @PermitAll @Operation(summary = "Créer un nouveau membre") @APIResponse(responseCode = "201", description = "Membre créé avec succès") @APIResponse(responseCode = "400", description = "Données invalides") diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/NotificationResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/NotificationResource.java new file mode 100644 index 0000000..3dc1f3c --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/NotificationResource.java @@ -0,0 +1,139 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.notification.NotificationDTO; +import dev.lions.unionflow.server.api.enums.notification.TypeNotification; +import dev.lions.unionflow.server.service.NotificationService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** Resource REST pour la gestion des notifications */ +@Path("/api/notifications") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@ApplicationScoped +@Tag(name = "Notifications", description = "API de gestion des notifications") +public class NotificationResource { + + private static final Logger LOG = Logger.getLogger(NotificationResource.class); + + @Inject NotificationService notificationService; + + @POST + @Operation(summary = "Envoyer une notification") + @APIResponse(responseCode = "200", description = "Notification envoyée") + public Response envoyerNotification(NotificationDTO notification) { + LOG.infof("Envoi de notification: %s", notification.getTitre()); + try { + CompletableFuture future = notificationService.envoyerNotification(notification); + NotificationDTO result = future.get(); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'envoi de notification"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("message", e.getMessage())) + .build(); + } + } + + @POST + @Path("/groupe") + @Operation(summary = "Envoyer une notification à un groupe") + @APIResponse(responseCode = "200", description = "Notifications envoyées") + public Response envoyerNotificationGroupe( + @QueryParam("type") TypeNotification type, + @QueryParam("titre") String titre, + @QueryParam("message") String message, + List destinatairesIds) { + LOG.infof("Envoi de notification de groupe: %d destinataires", destinatairesIds.size()); + try { + CompletableFuture> future = + notificationService.envoyerNotificationGroupe(type, titre, message, destinatairesIds, Map.of()); + List results = future.get(); + return Response.ok(results).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'envoi de notifications groupées"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("message", e.getMessage())) + .build(); + } + } + + @GET + @Path("/utilisateur/{utilisateurId}") + @Operation(summary = "Obtenir les notifications d'un utilisateur") + @APIResponse(responseCode = "200", description = "Notifications récupérées") + public Response obtenirNotifications( + @PathParam("utilisateurId") String utilisateurId, + @QueryParam("includeArchivees") @DefaultValue("false") boolean includeArchivees, + @QueryParam("limite") @DefaultValue("50") int limite) { + LOG.infof("Récupération notifications pour: %s", utilisateurId); + List notifications = + notificationService.obtenirNotificationsUtilisateur(utilisateurId, includeArchivees, limite); + return Response.ok(notifications).build(); + } + + @PUT + @Path("/{notificationId}/lue") + @Operation(summary = "Marquer une notification comme lue") + @APIResponse(responseCode = "200", description = "Notification marquée comme lue") + public Response marquerCommeLue( + @PathParam("notificationId") String notificationId, + @QueryParam("utilisateurId") String utilisateurId) { + LOG.infof("Marquage comme lue: %s", notificationId); + boolean succes = notificationService.marquerCommeLue(notificationId, utilisateurId); + return Response.ok(Map.of("success", succes)).build(); + } + + @PUT + @Path("/{notificationId}/archiver") + @Operation(summary = "Archiver une notification") + @APIResponse(responseCode = "200", description = "Notification archivée") + public Response archiverNotification( + @PathParam("notificationId") String notificationId, + @QueryParam("utilisateurId") String utilisateurId) { + LOG.infof("Archivage: %s", notificationId); + boolean succes = notificationService.archiverNotification(notificationId, utilisateurId); + return Response.ok(Map.of("success", succes)).build(); + } + + @GET + @Path("/stats") + @Operation(summary = "Obtenir les statistiques des notifications") + @APIResponse(responseCode = "200", description = "Statistiques récupérées") + public Response obtenirStatistiques() { + Map stats = notificationService.obtenirStatistiques(); + return Response.ok(stats).build(); + } + + @POST + @Path("/test/{utilisateurId}") + @Operation(summary = "Envoyer une notification de test") + @APIResponse(responseCode = "200", description = "Notification de test envoyée") + public Response envoyerNotificationTest( + @PathParam("utilisateurId") String utilisateurId, + @QueryParam("type") @DefaultValue("SYSTEME") TypeNotification type) { + LOG.infof("Envoi notification de test: %s", utilisateurId); + try { + CompletableFuture future = + notificationService.envoyerNotificationTest(utilisateurId, type); + NotificationDTO result = future.get(); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'envoi de notification de test"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("message", e.getMessage())) + .build(); + } + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java index 6373c23..310bb79 100644 --- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java @@ -89,6 +89,7 @@ public class OrganisationResource { /** Récupère toutes les organisations actives */ @GET + @jakarta.annotation.security.PermitAll // ✅ Accès public pour inscription @Operation( summary = "Lister les organisations", description = "Récupère la liste des organisations actives avec pagination") diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/PreferencesResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/PreferencesResource.java new file mode 100644 index 0000000..eaada05 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/PreferencesResource.java @@ -0,0 +1,75 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.service.PreferencesNotificationService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.Map; +import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** Resource REST pour la gestion des préférences utilisateur */ +@Path("/api/preferences") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@ApplicationScoped +@Tag(name = "Préférences", description = "API de gestion des préférences utilisateur") +public class PreferencesResource { + + private static final Logger LOG = Logger.getLogger(PreferencesResource.class); + + @Inject PreferencesNotificationService preferencesService; + + @GET + @Path("/{utilisateurId}") + @RolesAllowed({"USER", "ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Obtenir les préférences d'un utilisateur") + @APIResponse(responseCode = "200", description = "Préférences récupérées avec succès") + public Response obtenirPreferences( + @PathParam("utilisateurId") UUID utilisateurId) { + LOG.infof("Récupération des préférences pour l'utilisateur %s", utilisateurId); + Map preferences = preferencesService.obtenirPreferences(utilisateurId); + return Response.ok(preferences).build(); + } + + @PUT + @Path("/{utilisateurId}") + @RolesAllowed({"USER", "ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Mettre à jour les préférences d'un utilisateur") + @APIResponse(responseCode = "204", description = "Préférences mises à jour avec succès") + public Response mettreAJourPreferences( + @PathParam("utilisateurId") UUID utilisateurId, Map preferences) { + LOG.infof("Mise à jour des préférences pour l'utilisateur %s", utilisateurId); + preferencesService.mettreAJourPreferences(utilisateurId, preferences); + return Response.noContent().build(); + } + + @POST + @Path("/{utilisateurId}/reinitialiser") + @RolesAllowed({"USER", "ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Réinitialiser les préférences d'un utilisateur") + @APIResponse(responseCode = "204", description = "Préférences réinitialisées avec succès") + public Response reinitialiserPreferences(@PathParam("utilisateurId") UUID utilisateurId) { + LOG.infof("Réinitialisation des préférences pour l'utilisateur %s", utilisateurId); + preferencesService.reinitialiserPreferences(utilisateurId); + return Response.noContent().build(); + } + + @GET + @Path("/{utilisateurId}/export") + @RolesAllowed({"USER", "ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Exporter les préférences d'un utilisateur") + @APIResponse(responseCode = "200", description = "Préférences exportées avec succès") + public Response exporterPreferences(@PathParam("utilisateurId") UUID utilisateurId) { + LOG.infof("Export des préférences pour l'utilisateur %s", utilisateurId); + Map export = preferencesService.exporterPreferences(utilisateurId); + return Response.ok(export).build(); + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/TypeOrganisationResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/TypeOrganisationResource.java new file mode 100644 index 0000000..6983d53 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/TypeOrganisationResource.java @@ -0,0 +1,162 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.organisation.TypeOrganisationDTO; +import dev.lions.unionflow.server.service.TypeOrganisationService; +import jakarta.annotation.security.PermitAll; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.Map; +import java.util.UUID; +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; + +/** + * Ressource REST pour la gestion du catalogue des types d'organisation. + */ +@Path("/api/types-organisations") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Types d'organisation", description = "Catalogue des types d'organisation") +@PermitAll +public class TypeOrganisationResource { + + private static final Logger LOG = Logger.getLogger(TypeOrganisationResource.class); + + @Inject TypeOrganisationService service; + + /** Liste les types d'organisation. */ + @GET + @Operation( + summary = "Lister les types d'organisation", + description = "Récupère la liste des types d'organisation, optionnellement seulement actifs") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Liste des types récupérée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = TypeOrganisationDTO.class))), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response listTypes( + @Parameter(description = "Limiter aux types actifs", example = "true") + @QueryParam("onlyActifs") + @DefaultValue("true") + String onlyActifs) { + // Parsing manuel pour éviter toute erreur de conversion JAX-RS (qui peut renvoyer une 400) + boolean actifsSeulement = !"false".equalsIgnoreCase(onlyActifs); + List types = service.listAll(actifsSeulement); + return Response.ok(types).build(); + } + + /** Crée un nouveau type d'organisation (réservé à l'administration). */ + @POST + @Operation( + summary = "Créer un type d'organisation", + description = "Crée un nouveau type dans le catalogue (code doit exister dans l'enum)") + @APIResponses({ + @APIResponse( + responseCode = "201", + description = "Type créé avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = TypeOrganisationDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response create(TypeOrganisationDTO dto) { + try { + TypeOrganisationDTO created = service.create(dto); + return Response.status(Response.Status.CREATED).entity(created).build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Erreur lors de la création du type d'organisation: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de la création du type d'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Met à jour un type. */ + @PUT + @Path("/{id}") + @Operation( + summary = "Mettre à jour un type d'organisation", + description = "Met à jour un type existant (libellé, description, ordre, actif, code)") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Type mis à jour avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = TypeOrganisationDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Type non trouvé"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response update(@PathParam("id") UUID id, TypeOrganisationDTO dto) { + try { + TypeOrganisationDTO updated = service.update(id, dto); + return Response.ok(updated).build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Erreur lors de la mise à jour du type d'organisation: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de la mise à jour du type d'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Désactive un type (soft delete). */ + @DELETE + @Path("/{id}") + @Operation( + summary = "Désactiver un type d'organisation", + description = "Désactive un type dans le catalogue (soft delete)") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Type désactivé avec succès"), + @APIResponse(responseCode = "404", description = "Type non trouvé"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response disable(@PathParam("id") UUID id) { + try { + service.disable(id); + return Response.noContent().build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de la désactivation du type d'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } +} + + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java new file mode 100644 index 0000000..61a5526 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java @@ -0,0 +1,213 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.paiement.WaveBalanceDTO; +import dev.lions.unionflow.server.api.dto.paiement.WaveCheckoutSessionDTO; +import dev.lions.unionflow.server.api.dto.paiement.WaveWebhookDTO; +import dev.lions.unionflow.server.service.WaveService; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +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.tags.Tag; +import org.jboss.logging.Logger; + +/** + * Resource REST pour l'intégration Wave Money + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@Path("/api/wave") +@Tag(name = "Wave Money", description = "API d'intégration Wave Money pour les paiements") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class WaveResource { + + private static final Logger LOG = Logger.getLogger(WaveResource.class); + + @Inject WaveService waveService; + + @POST + @Path("/checkout/sessions") + @Operation( + summary = "Créer une session de paiement Wave", + description = "Crée une nouvelle session de paiement via l'API Wave Checkout") + @APIResponse( + responseCode = "200", + description = "Session créée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = WaveCheckoutSessionDTO.class))) + @APIResponse(responseCode = "400", description = "Données invalides") + @APIResponse(responseCode = "500", description = "Erreur serveur") + public Response creerSessionPaiement( + @Parameter(description = "Montant à payer", required = true) @QueryParam("montant") + @NotNull + @DecimalMin("0.01") + BigDecimal montant, + @Parameter(description = "Devise (XOF par défaut)") @QueryParam("devise") String devise, + @Parameter(description = "URL de succès", required = true) @QueryParam("successUrl") + @NotBlank + String successUrl, + @Parameter(description = "URL d'erreur", required = true) @QueryParam("errorUrl") + @NotBlank + String errorUrl, + @Parameter(description = "Référence UnionFlow") @QueryParam("reference") + String referenceUnionFlow, + @Parameter(description = "Description du paiement") @QueryParam("description") + String description, + @Parameter(description = "ID de l'organisation") @QueryParam("organisationId") UUID organisationId, + @Parameter(description = "ID du membre") @QueryParam("membreId") UUID membreId) { + try { + WaveCheckoutSessionDTO session = + waveService.creerSessionPaiement( + montant, + devise, + successUrl, + errorUrl, + referenceUnionFlow, + description, + organisationId, + membreId); + + return Response.ok(session).build(); + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création de la session: %s", e.getMessage()); + Map erreur = new HashMap<>(); + erreur.put("erreur", "Erreur lors de la création de la session"); + erreur.put("message", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build(); + } + } + + @GET + @Path("/checkout/sessions/{sessionId}") + @Operation( + summary = "Vérifier le statut d'une session", + description = "Récupère le statut d'une session de paiement Wave") + @APIResponse( + responseCode = "200", + description = "Statut récupéré avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = WaveCheckoutSessionDTO.class))) + @APIResponse(responseCode = "404", description = "Session non trouvée") + public Response verifierStatutSession( + @Parameter(description = "ID de la session Wave", required = true) @PathParam("sessionId") + @NotBlank + String sessionId) { + try { + WaveCheckoutSessionDTO session = waveService.verifierStatutSession(sessionId); + return Response.ok(session).build(); + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la vérification du statut: %s", e.getMessage()); + Map erreur = new HashMap<>(); + erreur.put("erreur", "Erreur lors de la vérification du statut"); + erreur.put("message", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build(); + } + } + + @POST + @Path("/webhooks") + @Operation( + summary = "Recevoir un webhook Wave", + description = "Endpoint pour recevoir les notifications webhook de Wave") + @APIResponse( + responseCode = "200", + description = "Webhook reçu et traité", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = WaveWebhookDTO.class))) + @APIResponse(responseCode = "400", description = "Webhook invalide") + @APIResponse(responseCode = "401", description = "Signature invalide") + @Consumes(MediaType.APPLICATION_JSON) + public Response recevoirWebhook( + @Parameter(description = "Payload du webhook", required = true) String payload, + @jakarta.ws.rs.HeaderParam("X-Wave-Signature") String signature) { + try { + // Récupérer les headers + Map headers = new HashMap<>(); + if (signature != null) { + headers.put("X-Wave-Signature", signature); + } + + WaveWebhookDTO webhook = waveService.traiterWebhook(payload, signature, headers); + return Response.ok(webhook).build(); + + } catch (SecurityException e) { + LOG.warnf("Signature webhook invalide: %s", e.getMessage()); + return Response.status(Response.Status.UNAUTHORIZED).build(); + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du traitement du webhook: %s", e.getMessage()); + Map erreur = new HashMap<>(); + erreur.put("erreur", "Erreur lors du traitement du webhook"); + erreur.put("message", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build(); + } + } + + @GET + @Path("/balance") + @Operation( + summary = "Consulter le solde Wave", + description = "Récupère le solde disponible du wallet Wave") + @APIResponse( + responseCode = "200", + description = "Solde récupéré avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = WaveBalanceDTO.class))) + public Response consulterSolde() { + try { + WaveBalanceDTO balance = waveService.consulterSolde(); + return Response.ok(balance).build(); + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la consultation du solde: %s", e.getMessage()); + Map erreur = new HashMap<>(); + erreur.put("erreur", "Erreur lors de la consultation du solde"); + erreur.put("message", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build(); + } + } + + @GET + @Path("/test") + @Operation( + summary = "Tester la connexion Wave", + description = "Teste la connexion et la configuration de l'API Wave") + @APIResponse(responseCode = "200", description = "Test effectué") + public Response testerConnexion() { + Map resultat = waveService.testerConnexion(); + return Response.ok(resultat).build(); + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AdhesionService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AdhesionService.java new file mode 100644 index 0000000..55aa855 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AdhesionService.java @@ -0,0 +1,559 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.finance.AdhesionDTO; +import dev.lions.unionflow.server.entity.Adhesion; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.AdhesionRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +/** + * Service métier pour la gestion des adhésions + * Contient la logique métier et les règles de validation + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@ApplicationScoped +@Slf4j +public class AdhesionService { + + @Inject AdhesionRepository adhesionRepository; + + @Inject MembreRepository membreRepository; + + @Inject OrganisationRepository organisationRepository; + + /** + * Récupère toutes les adhésions avec pagination + * + * @param page numéro de page (0-based) + * @param size taille de la page + * @return liste des adhésions converties en DTO + */ + public List getAllAdhesions(int page, int size) { + log.debug("Récupération des adhésions - page: {}, size: {}", page, size); + + jakarta.persistence.TypedQuery query = + adhesionRepository + .getEntityManager() + .createQuery( + "SELECT a FROM Adhesion a ORDER BY a.dateDemande DESC", Adhesion.class); + query.setFirstResult(page * size); + query.setMaxResults(size); + List adhesions = query.getResultList(); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère une adhésion par son ID + * + * @param id identifiant UUID de l'adhésion + * @return DTO de l'adhésion + * @throws NotFoundException si l'adhésion n'existe pas + */ + public AdhesionDTO getAdhesionById(@NotNull UUID id) { + log.debug("Récupération de l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + return convertToDTO(adhesion); + } + + /** + * Récupère une adhésion par son numéro de référence + * + * @param numeroReference numéro de référence unique + * @return DTO de l'adhésion + * @throws NotFoundException si l'adhésion n'existe pas + */ + public AdhesionDTO getAdhesionByReference(@NotNull String numeroReference) { + log.debug("Récupération de l'adhésion avec référence: {}", numeroReference); + + Adhesion adhesion = + adhesionRepository + .findByNumeroReference(numeroReference) + .orElseThrow( + () -> + new NotFoundException( + "Adhésion non trouvée avec la référence: " + numeroReference)); + + return convertToDTO(adhesion); + } + + /** + * Crée une nouvelle adhésion + * + * @param adhesionDTO données de l'adhésion à créer + * @return DTO de l'adhésion créée + */ + @Transactional + public AdhesionDTO createAdhesion(@Valid AdhesionDTO adhesionDTO) { + log.info( + "Création d'une nouvelle adhésion pour le membre: {} et l'organisation: {}", + adhesionDTO.getMembreId(), + adhesionDTO.getOrganisationId()); + + // Validation du membre + Membre membre = + membreRepository + .findByIdOptional(adhesionDTO.getMembreId()) + .orElseThrow( + () -> + new NotFoundException( + "Membre non trouvé avec l'ID: " + adhesionDTO.getMembreId())); + + // Validation de l'organisation + Organisation organisation = + organisationRepository + .findByIdOptional(adhesionDTO.getOrganisationId()) + .orElseThrow( + () -> + new NotFoundException( + "Organisation non trouvée avec l'ID: " + adhesionDTO.getOrganisationId())); + + // Conversion DTO vers entité + Adhesion adhesion = convertToEntity(adhesionDTO); + adhesion.setMembre(membre); + adhesion.setOrganisation(organisation); + + // Génération automatique du numéro de référence si absent + if (adhesion.getNumeroReference() == null || adhesion.getNumeroReference().isEmpty()) { + adhesion.setNumeroReference(genererNumeroReference()); + } + + // Initialisation par défaut + if (adhesion.getDateDemande() == null) { + adhesion.setDateDemande(LocalDate.now()); + } + if (adhesion.getStatut() == null || adhesion.getStatut().isEmpty()) { + adhesion.setStatut("EN_ATTENTE"); + } + if (adhesion.getMontantPaye() == null) { + adhesion.setMontantPaye(BigDecimal.ZERO); + } + if (adhesion.getCodeDevise() == null || adhesion.getCodeDevise().isEmpty()) { + adhesion.setCodeDevise("XOF"); + } + + // Persistance + adhesionRepository.persist(adhesion); + + log.info( + "Adhésion créée avec succès - ID: {}, Référence: {}", + adhesion.getId(), + adhesion.getNumeroReference()); + + return convertToDTO(adhesion); + } + + /** + * Met à jour une adhésion existante + * + * @param id identifiant UUID de l'adhésion + * @param adhesionDTO nouvelles données + * @return DTO de l'adhésion mise à jour + */ + @Transactional + public AdhesionDTO updateAdhesion(@NotNull UUID id, @Valid AdhesionDTO adhesionDTO) { + log.info("Mise à jour de l'adhésion avec ID: {}", id); + + Adhesion adhesionExistante = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + // Mise à jour des champs modifiables + updateAdhesionFields(adhesionExistante, adhesionDTO); + + log.info("Adhésion mise à jour avec succès - ID: {}", id); + + return convertToDTO(adhesionExistante); + } + + /** + * Supprime (désactive) une adhésion + * + * @param id identifiant UUID de l'adhésion + */ + @Transactional + public void deleteAdhesion(@NotNull UUID id) { + log.info("Suppression de l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + // Vérification si l'adhésion peut être supprimée + if ("PAYEE".equals(adhesion.getStatut())) { + throw new IllegalStateException("Impossible de supprimer une adhésion déjà payée"); + } + + adhesion.setStatut("ANNULEE"); + + log.info("Adhésion supprimée avec succès - ID: {}", id); + } + + /** + * Approuve une adhésion + * + * @param id identifiant UUID de l'adhésion + * @param approuvePar nom de l'utilisateur qui approuve + * @return DTO de l'adhésion approuvée + */ + @Transactional + public AdhesionDTO approuverAdhesion(@NotNull UUID id, String approuvePar) { + log.info("Approbation de l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + if (!"EN_ATTENTE".equals(adhesion.getStatut())) { + throw new IllegalStateException( + "Seules les adhésions en attente peuvent être approuvées"); + } + + adhesion.setStatut("APPROUVEE"); + adhesion.setDateApprobation(LocalDate.now()); + adhesion.setApprouvePar(approuvePar); + adhesion.setDateValidation(LocalDate.now()); + + log.info("Adhésion approuvée avec succès - ID: {}", id); + + return convertToDTO(adhesion); + } + + /** + * Rejette une adhésion + * + * @param id identifiant UUID de l'adhésion + * @param motifRejet motif du rejet + * @return DTO de l'adhésion rejetée + */ + @Transactional + public AdhesionDTO rejeterAdhesion(@NotNull UUID id, String motifRejet) { + log.info("Rejet de l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + if (!"EN_ATTENTE".equals(adhesion.getStatut())) { + throw new IllegalStateException("Seules les adhésions en attente peuvent être rejetées"); + } + + adhesion.setStatut("REJETEE"); + adhesion.setMotifRejet(motifRejet); + + log.info("Adhésion rejetée avec succès - ID: {}", id); + + return convertToDTO(adhesion); + } + + /** + * Enregistre un paiement pour une adhésion + * + * @param id identifiant UUID de l'adhésion + * @param montantPaye montant payé + * @param methodePaiement méthode de paiement + * @param referencePaiement référence du paiement + * @return DTO de l'adhésion mise à jour + */ + @Transactional + public AdhesionDTO enregistrerPaiement( + @NotNull UUID id, + BigDecimal montantPaye, + String methodePaiement, + String referencePaiement) { + log.info("Enregistrement du paiement pour l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + if (!"APPROUVEE".equals(adhesion.getStatut()) && !"EN_PAIEMENT".equals(adhesion.getStatut())) { + throw new IllegalStateException( + "Seules les adhésions approuvées peuvent recevoir un paiement"); + } + + BigDecimal nouveauMontantPaye = + adhesion.getMontantPaye() != null + ? adhesion.getMontantPaye().add(montantPaye) + : montantPaye; + + adhesion.setMontantPaye(nouveauMontantPaye); + adhesion.setMethodePaiement(methodePaiement); + adhesion.setReferencePaiement(referencePaiement); + adhesion.setDatePaiement(java.time.LocalDateTime.now()); + + // Mise à jour du statut si payée intégralement + if (adhesion.isPayeeIntegralement()) { + adhesion.setStatut("PAYEE"); + } else { + adhesion.setStatut("EN_PAIEMENT"); + } + + log.info("Paiement enregistré avec succès pour l'adhésion - ID: {}", id); + + return convertToDTO(adhesion); + } + + /** + * Récupère les adhésions d'un membre + * + * @param membreId identifiant UUID du membre + * @param page numéro de page + * @param size taille de la page + * @return liste des adhésions du membre + */ + public List getAdhesionsByMembre(@NotNull UUID membreId, int page, int size) { + log.debug("Récupération des adhésions du membre: {}", membreId); + + if (!membreRepository.findByIdOptional(membreId).isPresent()) { + throw new NotFoundException("Membre non trouvé avec l'ID: " + membreId); + } + + List adhesions = + adhesionRepository.findByMembreId(membreId).stream() + .skip(page * size) + .limit(size) + .collect(Collectors.toList()); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les adhésions d'une organisation + * + * @param organisationId identifiant UUID de l'organisation + * @param page numéro de page + * @param size taille de la page + * @return liste des adhésions de l'organisation + */ + public List getAdhesionsByOrganisation( + @NotNull UUID organisationId, int page, int size) { + log.debug("Récupération des adhésions de l'organisation: {}", organisationId); + + if (!organisationRepository.findByIdOptional(organisationId).isPresent()) { + throw new NotFoundException("Organisation non trouvée avec l'ID: " + organisationId); + } + + List adhesions = + adhesionRepository.findByOrganisationId(organisationId).stream() + .skip(page * size) + .limit(size) + .collect(Collectors.toList()); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les adhésions par statut + * + * @param statut statut recherché + * @param page numéro de page + * @param size taille de la page + * @return liste des adhésions avec le statut spécifié + */ + public List getAdhesionsByStatut(@NotNull String statut, int page, int size) { + log.debug("Récupération des adhésions avec statut: {}", statut); + + List adhesions = + adhesionRepository.findByStatut(statut).stream() + .skip(page * size) + .limit(size) + .collect(Collectors.toList()); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les adhésions en attente + * + * @param page numéro de page + * @param size taille de la page + * @return liste des adhésions en attente + */ + public List getAdhesionsEnAttente(int page, int size) { + log.debug("Récupération des adhésions en attente"); + + List adhesions = + adhesionRepository.findEnAttente().stream() + .skip(page * size) + .limit(size) + .collect(Collectors.toList()); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les statistiques des adhésions + * + * @return map contenant les statistiques + */ + public Map getStatistiquesAdhesions() { + log.debug("Calcul des statistiques des adhésions"); + + long totalAdhesions = adhesionRepository.count(); + long adhesionsApprouvees = adhesionRepository.findByStatut("APPROUVEE").size(); + long adhesionsEnAttente = adhesionRepository.findEnAttente().size(); + long adhesionsPayees = adhesionRepository.findByStatut("PAYEE").size(); + + return Map.of( + "totalAdhesions", totalAdhesions, + "adhesionsApprouvees", adhesionsApprouvees, + "adhesionsEnAttente", adhesionsEnAttente, + "adhesionsPayees", adhesionsPayees, + "tauxApprobation", + totalAdhesions > 0 ? (adhesionsApprouvees * 100.0 / totalAdhesions) : 0.0, + "tauxPaiement", + adhesionsApprouvees > 0 + ? (adhesionsPayees * 100.0 / adhesionsApprouvees) + : 0.0); + } + + /** Génère un numéro de référence unique pour une adhésion */ + private String genererNumeroReference() { + return "ADH-" + System.currentTimeMillis() + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + } + + /** Convertit une entité Adhesion en DTO */ + private AdhesionDTO convertToDTO(Adhesion adhesion) { + if (adhesion == null) { + return null; + } + + AdhesionDTO dto = new AdhesionDTO(); + + dto.setId(adhesion.getId()); + dto.setNumeroReference(adhesion.getNumeroReference()); + + // Conversion du membre associé + if (adhesion.getMembre() != null) { + dto.setMembreId(adhesion.getMembre().getId()); + dto.setNomMembre(adhesion.getMembre().getNomComplet()); + dto.setNumeroMembre(adhesion.getMembre().getNumeroMembre()); + dto.setEmailMembre(adhesion.getMembre().getEmail()); + } + + // Conversion de l'organisation + if (adhesion.getOrganisation() != null) { + dto.setOrganisationId(adhesion.getOrganisation().getId()); + dto.setNomOrganisation(adhesion.getOrganisation().getNom()); + } + + // Propriétés de l'adhésion + dto.setDateDemande(adhesion.getDateDemande()); + dto.setFraisAdhesion(adhesion.getFraisAdhesion()); + dto.setMontantPaye(adhesion.getMontantPaye()); + dto.setCodeDevise(adhesion.getCodeDevise()); + dto.setStatut(adhesion.getStatut()); + dto.setDateApprobation(adhesion.getDateApprobation()); + dto.setDatePaiement(adhesion.getDatePaiement()); + dto.setMethodePaiement(adhesion.getMethodePaiement()); + dto.setReferencePaiement(adhesion.getReferencePaiement()); + dto.setMotifRejet(adhesion.getMotifRejet()); + dto.setObservations(adhesion.getObservations()); + dto.setApprouvePar(adhesion.getApprouvePar()); + dto.setDateValidation(adhesion.getDateValidation()); + + // Métadonnées de BaseEntity + dto.setDateCreation(adhesion.getDateCreation()); + dto.setDateModification(adhesion.getDateModification()); + dto.setCreePar(adhesion.getCreePar()); + dto.setModifiePar(adhesion.getModifiePar()); + dto.setActif(adhesion.getActif()); + + return dto; + } + + /** Convertit un DTO en entité Adhesion */ + private Adhesion convertToEntity(AdhesionDTO dto) { + if (dto == null) { + return null; + } + + Adhesion adhesion = new Adhesion(); + + adhesion.setNumeroReference(dto.getNumeroReference()); + adhesion.setDateDemande(dto.getDateDemande()); + adhesion.setFraisAdhesion(dto.getFraisAdhesion()); + adhesion.setMontantPaye(dto.getMontantPaye() != null ? dto.getMontantPaye() : BigDecimal.ZERO); + adhesion.setCodeDevise(dto.getCodeDevise()); + adhesion.setStatut(dto.getStatut()); + adhesion.setDateApprobation(dto.getDateApprobation()); + adhesion.setDatePaiement(dto.getDatePaiement()); + adhesion.setMethodePaiement(dto.getMethodePaiement()); + adhesion.setReferencePaiement(dto.getReferencePaiement()); + adhesion.setMotifRejet(dto.getMotifRejet()); + adhesion.setObservations(dto.getObservations()); + adhesion.setApprouvePar(dto.getApprouvePar()); + adhesion.setDateValidation(dto.getDateValidation()); + + return adhesion; + } + + /** Met à jour les champs modifiables d'une adhésion existante */ + private void updateAdhesionFields(Adhesion adhesion, AdhesionDTO dto) { + if (dto.getFraisAdhesion() != null) { + adhesion.setFraisAdhesion(dto.getFraisAdhesion()); + } + if (dto.getMontantPaye() != null) { + adhesion.setMontantPaye(dto.getMontantPaye()); + } + if (dto.getStatut() != null) { + adhesion.setStatut(dto.getStatut()); + } + if (dto.getDateApprobation() != null) { + adhesion.setDateApprobation(dto.getDateApprobation()); + } + if (dto.getDatePaiement() != null) { + adhesion.setDatePaiement(dto.getDatePaiement()); + } + if (dto.getMethodePaiement() != null) { + adhesion.setMethodePaiement(dto.getMethodePaiement()); + } + if (dto.getReferencePaiement() != null) { + adhesion.setReferencePaiement(dto.getReferencePaiement()); + } + if (dto.getMotifRejet() != null) { + adhesion.setMotifRejet(dto.getMotifRejet()); + } + if (dto.getObservations() != null) { + adhesion.setObservations(dto.getObservations()); + } + if (dto.getApprouvePar() != null) { + adhesion.setApprouvePar(dto.getApprouvePar()); + } + if (dto.getDateValidation() != null) { + adhesion.setDateValidation(dto.getDateValidation()); + } + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AuditService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AuditService.java new file mode 100644 index 0000000..a2fb126 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/AuditService.java @@ -0,0 +1,229 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.admin.AuditLogDTO; +import dev.lions.unionflow.server.entity.AuditLog; +import dev.lions.unionflow.server.repository.AuditLogRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +/** + * Service pour la gestion des logs d'audit + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@ApplicationScoped +@Slf4j +public class AuditService { + + @Inject + AuditLogRepository auditLogRepository; + + /** + * Enregistre un nouveau log d'audit + */ + @Transactional + public AuditLogDTO enregistrerLog(AuditLogDTO dto) { + log.debug("Enregistrement d'un log d'audit: {}", dto.getTypeAction()); + + AuditLog auditLog = convertToEntity(dto); + auditLogRepository.persist(auditLog); + + return convertToDTO(auditLog); + } + + /** + * Récupère tous les logs avec pagination + */ + public Map listerTous(int page, int size, String sortBy, String sortOrder) { + log.debug("Récupération des logs d'audit - page: {}, size: {}", page, size); + + String orderBy = sortBy != null ? sortBy : "dateHeure"; + String order = "desc".equalsIgnoreCase(sortOrder) ? "DESC" : "ASC"; + + var entityManager = auditLogRepository.getEntityManager(); + + // Compter le total + long total = auditLogRepository.count(); + + // Récupérer les logs avec pagination + var query = entityManager.createQuery( + "SELECT a FROM AuditLog a ORDER BY a." + orderBy + " " + order, AuditLog.class); + query.setFirstResult(page * size); + query.setMaxResults(size); + + List logs = query.getResultList(); + List dtos = logs.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return Map.of( + "data", dtos, + "total", total, + "page", page, + "size", size, + "totalPages", (int) Math.ceil((double) total / size) + ); + } + + /** + * Recherche les logs avec filtres + */ + public Map rechercher( + LocalDateTime dateDebut, LocalDateTime dateFin, + String typeAction, String severite, String utilisateur, + String module, String ipAddress, + int page, int size) { + + log.debug("Recherche de logs d'audit avec filtres"); + + // Construire la requête dynamique avec Criteria API + var entityManager = auditLogRepository.getEntityManager(); + var cb = entityManager.getCriteriaBuilder(); + var query = cb.createQuery(AuditLog.class); + var root = query.from(AuditLog.class); + + var predicates = new ArrayList(); + + if (dateDebut != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("dateHeure"), dateDebut)); + } + if (dateFin != null) { + predicates.add(cb.lessThanOrEqualTo(root.get("dateHeure"), dateFin)); + } + if (typeAction != null && !typeAction.isEmpty()) { + predicates.add(cb.equal(root.get("typeAction"), typeAction)); + } + if (severite != null && !severite.isEmpty()) { + predicates.add(cb.equal(root.get("severite"), severite)); + } + if (utilisateur != null && !utilisateur.isEmpty()) { + predicates.add(cb.like(cb.lower(root.get("utilisateur")), + "%" + utilisateur.toLowerCase() + "%")); + } + if (module != null && !module.isEmpty()) { + predicates.add(cb.equal(root.get("module"), module)); + } + if (ipAddress != null && !ipAddress.isEmpty()) { + predicates.add(cb.like(root.get("ipAddress"), "%" + ipAddress + "%")); + } + + query.where(predicates.toArray(new jakarta.persistence.criteria.Predicate[0])); + query.orderBy(cb.desc(root.get("dateHeure"))); + + // Compter le total + var countQuery = cb.createQuery(Long.class); + countQuery.select(cb.count(countQuery.from(AuditLog.class))); + countQuery.where(predicates.toArray(new jakarta.persistence.criteria.Predicate[0])); + long total = entityManager.createQuery(countQuery).getSingleResult(); + + // Récupérer les résultats avec pagination + var typedQuery = entityManager.createQuery(query); + typedQuery.setFirstResult(page * size); + typedQuery.setMaxResults(size); + + List logs = typedQuery.getResultList(); + List dtos = logs.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return Map.of( + "data", dtos, + "total", total, + "page", page, + "size", size, + "totalPages", (int) Math.ceil((double) total / size) + ); + } + + /** + * Récupère les statistiques d'audit + */ + public Map getStatistiques() { + long total = auditLogRepository.count(); + + var entityManager = auditLogRepository.getEntityManager(); + + long success = entityManager.createQuery( + "SELECT COUNT(a) FROM AuditLog a WHERE a.severite = :severite", Long.class) + .setParameter("severite", "SUCCESS") + .getSingleResult(); + + long errors = entityManager.createQuery( + "SELECT COUNT(a) FROM AuditLog a WHERE a.severite IN :severites", Long.class) + .setParameter("severites", List.of("ERROR", "CRITICAL")) + .getSingleResult(); + + long warnings = entityManager.createQuery( + "SELECT COUNT(a) FROM AuditLog a WHERE a.severite = :severite", Long.class) + .setParameter("severite", "WARNING") + .getSingleResult(); + + return Map.of( + "total", total, + "success", success, + "errors", errors, + "warnings", warnings + ); + } + + /** + * Convertit une entité en DTO + */ + private AuditLogDTO convertToDTO(AuditLog auditLog) { + AuditLogDTO dto = new AuditLogDTO(); + dto.setId(auditLog.getId()); + dto.setTypeAction(auditLog.getTypeAction()); + dto.setSeverite(auditLog.getSeverite()); + dto.setUtilisateur(auditLog.getUtilisateur()); + dto.setRole(auditLog.getRole()); + dto.setModule(auditLog.getModule()); + dto.setDescription(auditLog.getDescription()); + dto.setDetails(auditLog.getDetails()); + dto.setIpAddress(auditLog.getIpAddress()); + dto.setUserAgent(auditLog.getUserAgent()); + dto.setSessionId(auditLog.getSessionId()); + dto.setDateHeure(auditLog.getDateHeure()); + dto.setDonneesAvant(auditLog.getDonneesAvant()); + dto.setDonneesApres(auditLog.getDonneesApres()); + dto.setEntiteId(auditLog.getEntiteId()); + dto.setEntiteType(auditLog.getEntiteType()); + return dto; + } + + /** + * Convertit un DTO en entité + */ + private AuditLog convertToEntity(AuditLogDTO dto) { + AuditLog auditLog = new AuditLog(); + if (dto.getId() != null) { + auditLog.setId(dto.getId()); + } + auditLog.setTypeAction(dto.getTypeAction()); + auditLog.setSeverite(dto.getSeverite()); + auditLog.setUtilisateur(dto.getUtilisateur()); + auditLog.setRole(dto.getRole()); + auditLog.setModule(dto.getModule()); + auditLog.setDescription(dto.getDescription()); + auditLog.setDetails(dto.getDetails()); + auditLog.setIpAddress(dto.getIpAddress()); + auditLog.setUserAgent(dto.getUserAgent()); + auditLog.setSessionId(dto.getSessionId()); + auditLog.setDateHeure(dto.getDateHeure() != null ? dto.getDateHeure() : LocalDateTime.now()); + auditLog.setDonneesAvant(dto.getDonneesAvant()); + auditLog.setDonneesApres(dto.getDonneesApres()); + auditLog.setEntiteId(dto.getEntiteId()); + auditLog.setEntiteType(dto.getEntiteType()); + return auditLog; + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/ExportService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/ExportService.java new file mode 100644 index 0000000..659e86d --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/ExportService.java @@ -0,0 +1,237 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.finance.CotisationDTO; +import dev.lions.unionflow.server.entity.Cotisation; +import dev.lions.unionflow.server.repository.CotisationRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.jboss.logging.Logger; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * Service d'export des données en Excel et PDF + * + * @author UnionFlow Team + * @version 1.0 + */ +@ApplicationScoped +public class ExportService { + + private static final Logger LOG = Logger.getLogger(ExportService.class); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"); + + @Inject + CotisationRepository cotisationRepository; + + @Inject + CotisationService cotisationService; + + /** + * Exporte les cotisations en format CSV (compatible Excel) + */ + public byte[] exporterCotisationsCSV(List cotisationIds) { + LOG.infof("Export CSV de %d cotisations", cotisationIds.size()); + + StringBuilder csv = new StringBuilder(); + csv.append("Numéro Référence;Membre;Type;Montant Dû;Montant Payé;Statut;Date Échéance;Date Paiement;Méthode Paiement\n"); + + for (UUID id : cotisationIds) { + Optional cotisationOpt = cotisationRepository.findByIdOptional(id); + if (cotisationOpt.isPresent()) { + Cotisation c = cotisationOpt.get(); + String nomMembre = c.getMembre() != null + ? c.getMembre().getNom() + " " + c.getMembre().getPrenom() + : ""; + csv.append(String.format("%s;%s;%s;%s;%s;%s;%s;%s;%s\n", + c.getNumeroReference() != null ? c.getNumeroReference() : "", + nomMembre, + c.getTypeCotisation() != null ? c.getTypeCotisation() : "", + c.getMontantDu() != null ? c.getMontantDu().toString() : "0", + c.getMontantPaye() != null ? c.getMontantPaye().toString() : "0", + c.getStatut() != null ? c.getStatut() : "", + c.getDateEcheance() != null ? c.getDateEcheance().format(DATE_FORMATTER) : "", + c.getDatePaiement() != null ? c.getDatePaiement().format(DATETIME_FORMATTER) : "", + c.getMethodePaiement() != null ? c.getMethodePaiement() : "" + )); + } + } + + return csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + /** + * Exporte toutes les cotisations filtrées en CSV + */ + public byte[] exporterToutesCotisationsCSV(String statut, String type, UUID associationId) { + LOG.info("Export CSV de toutes les cotisations"); + + List cotisations = cotisationRepository.listAll(); + + // Filtrer + if (statut != null && !statut.isEmpty()) { + cotisations = cotisations.stream() + .filter(c -> c.getStatut() != null && c.getStatut().equals(statut)) + .toList(); + } + if (type != null && !type.isEmpty()) { + cotisations = cotisations.stream() + .filter(c -> c.getTypeCotisation() != null && c.getTypeCotisation().equals(type)) + .toList(); + } + // Note: le filtrage par association n'est pas disponible car Membre n'a pas de lien direct + // avec Association dans cette version du modèle + + List ids = cotisations.stream().map(Cotisation::getId).toList(); + return exporterCotisationsCSV(ids); + } + + /** + * Génère un reçu de paiement en format texte (pour impression) + */ + public byte[] genererRecuPaiement(UUID cotisationId) { + LOG.infof("Génération reçu pour cotisation: %s", cotisationId); + + Optional cotisationOpt = cotisationRepository.findByIdOptional(cotisationId); + if (cotisationOpt.isEmpty()) { + return "Cotisation non trouvée".getBytes(); + } + + Cotisation c = cotisationOpt.get(); + + StringBuilder recu = new StringBuilder(); + recu.append("═══════════════════════════════════════════════════════════════\n"); + recu.append(" REÇU DE PAIEMENT\n"); + recu.append("═══════════════════════════════════════════════════════════════\n\n"); + + recu.append("Numéro de reçu : ").append(c.getNumeroReference()).append("\n"); + recu.append("Date : ").append(LocalDateTime.now().format(DATETIME_FORMATTER)).append("\n\n"); + + recu.append("───────────────────────────────────────────────────────────────\n"); + recu.append(" INFORMATIONS MEMBRE\n"); + recu.append("───────────────────────────────────────────────────────────────\n"); + + if (c.getMembre() != null) { + recu.append("Nom : ").append(c.getMembre().getNom()).append(" ").append(c.getMembre().getPrenom()).append("\n"); + recu.append("Numéro membre : ").append(c.getMembre().getNumeroMembre()).append("\n"); + } + + recu.append("\n───────────────────────────────────────────────────────────────\n"); + recu.append(" DÉTAILS DU PAIEMENT\n"); + recu.append("───────────────────────────────────────────────────────────────\n"); + + recu.append("Type cotisation : ").append(c.getTypeCotisation() != null ? c.getTypeCotisation() : "").append("\n"); + recu.append("Période : ").append(c.getPeriode() != null ? c.getPeriode() : "").append("\n"); + recu.append("Montant dû : ").append(formatMontant(c.getMontantDu())).append("\n"); + recu.append("Montant payé : ").append(formatMontant(c.getMontantPaye())).append("\n"); + recu.append("Mode de paiement : ").append(c.getMethodePaiement() != null ? c.getMethodePaiement() : "").append("\n"); + recu.append("Date de paiement : ").append(c.getDatePaiement() != null ? c.getDatePaiement().format(DATETIME_FORMATTER) : "").append("\n"); + recu.append("Statut : ").append(c.getStatut() != null ? c.getStatut() : "").append("\n"); + + recu.append("\n═══════════════════════════════════════════════════════════════\n"); + recu.append(" Ce document fait foi de paiement de cotisation\n"); + recu.append(" Merci de votre confiance !\n"); + recu.append("═══════════════════════════════════════════════════════════════\n"); + + return recu.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + /** + * Génère plusieurs reçus de paiement + */ + public byte[] genererRecusGroupes(List cotisationIds) { + LOG.infof("Génération de %d reçus groupés", cotisationIds.size()); + + StringBuilder allRecus = new StringBuilder(); + for (int i = 0; i < cotisationIds.size(); i++) { + byte[] recu = genererRecuPaiement(cotisationIds.get(i)); + allRecus.append(new String(recu, java.nio.charset.StandardCharsets.UTF_8)); + if (i < cotisationIds.size() - 1) { + allRecus.append("\n\n════════════════════════ PAGE SUIVANTE ════════════════════════\n\n"); + } + } + + return allRecus.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + /** + * Génère un rapport mensuel + */ + public byte[] genererRapportMensuel(int annee, int mois, UUID associationId) { + LOG.infof("Génération rapport mensuel: %d/%d", mois, annee); + + List cotisations = cotisationRepository.listAll(); + + // Filtrer par mois/année et association + LocalDate debut = LocalDate.of(annee, mois, 1); + LocalDate fin = debut.plusMonths(1).minusDays(1); + + cotisations = cotisations.stream() + .filter(c -> { + if (c.getDateCreation() == null) return false; + LocalDate dateCot = c.getDateCreation().toLocalDate(); + return !dateCot.isBefore(debut) && !dateCot.isAfter(fin); + }) + // Note: le filtrage par association n'est pas implémenté ici + .toList(); + + // Calculer les statistiques + long total = cotisations.size(); + long payees = cotisations.stream().filter(c -> "PAYEE".equals(c.getStatut())).count(); + long enAttente = cotisations.stream().filter(c -> "EN_ATTENTE".equals(c.getStatut())).count(); + long enRetard = cotisations.stream().filter(c -> "EN_RETARD".equals(c.getStatut())).count(); + + BigDecimal montantTotal = cotisations.stream() + .map(c -> c.getMontantDu() != null ? c.getMontantDu() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal montantCollecte = cotisations.stream() + .filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut())) + .map(c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + double tauxRecouvrement = montantTotal.compareTo(BigDecimal.ZERO) > 0 + ? montantCollecte.multiply(BigDecimal.valueOf(100)).divide(montantTotal, 2, java.math.RoundingMode.HALF_UP).doubleValue() + : 0; + + // Construire le rapport + StringBuilder rapport = new StringBuilder(); + rapport.append("═══════════════════════════════════════════════════════════════\n"); + rapport.append(" RAPPORT MENSUEL DES COTISATIONS\n"); + rapport.append("═══════════════════════════════════════════════════════════════\n\n"); + + rapport.append("Période : ").append(String.format("%02d/%d", mois, annee)).append("\n"); + rapport.append("Date de génération: ").append(LocalDateTime.now().format(DATETIME_FORMATTER)).append("\n\n"); + + rapport.append("───────────────────────────────────────────────────────────────\n"); + rapport.append(" RÉSUMÉ\n"); + rapport.append("───────────────────────────────────────────────────────────────\n\n"); + + rapport.append("Total cotisations : ").append(total).append("\n"); + rapport.append("Cotisations payées : ").append(payees).append("\n"); + rapport.append("Cotisations en attente: ").append(enAttente).append("\n"); + rapport.append("Cotisations en retard : ").append(enRetard).append("\n\n"); + + rapport.append("───────────────────────────────────────────────────────────────\n"); + rapport.append(" FINANCIER\n"); + rapport.append("───────────────────────────────────────────────────────────────\n\n"); + + rapport.append("Montant total attendu : ").append(formatMontant(montantTotal)).append("\n"); + rapport.append("Montant collecté : ").append(formatMontant(montantCollecte)).append("\n"); + rapport.append("Taux de recouvrement : ").append(String.format("%.1f%%", tauxRecouvrement)).append("\n\n"); + + rapport.append("═══════════════════════════════════════════════════════════════\n"); + + return rapport.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + private String formatMontant(BigDecimal montant) { + if (montant == null) return "0 FCFA"; + return String.format("%,.0f FCFA", montant.doubleValue()); + } +} diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/TypeOrganisationService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/TypeOrganisationService.java new file mode 100644 index 0000000..3e3518a --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/TypeOrganisationService.java @@ -0,0 +1,146 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.organisation.TypeOrganisationDTO; +import dev.lions.unionflow.server.entity.TypeOrganisationEntity; +import dev.lions.unionflow.server.repository.TypeOrganisationRepository; +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + + /** + * Service de gestion du catalogue des types d'organisation. + * + *

Synchronise les types persistés avec l'enum {@link TypeOrganisation} pour les valeurs + * par défaut, puis permet un CRUD entièrement dynamique (les nouveaux codes ne sont plus + * limités aux valeurs de l'enum). + */ +@ApplicationScoped +public class TypeOrganisationService { + + private static final Logger LOG = Logger.getLogger(TypeOrganisationService.class); + + @Inject TypeOrganisationRepository repository; + @Inject KeycloakService keycloakService; + + // Plus d'initialisation automatique : le catalogue des types est désormais entièrement + // géré en mode CRUD via l'UI d'administration. Aucune donnée fictive n'est injectée + // au démarrage ; si nécessaire, utilisez des scripts de migration (Flyway) ou l'UI. + + /** Retourne la liste de tous les types (optionnellement seulement actifs). */ + public List listAll(boolean onlyActifs) { + List entities = + onlyActifs ? repository.listActifsOrdennes() : repository.listAll(); + return entities.stream().map(this::toDTO).collect(Collectors.toList()); + } + + /** Crée un nouveau type. Le code doit être non vide et unique. */ + @Transactional + public TypeOrganisationDTO create(TypeOrganisationDTO dto) { + validateCode(dto.getCode()); + + // Si un type existe déjà pour ce code, on retourne simplement l'existant + // (comportement idempotent côté API) plutôt que de remonter une 400. + // Le CRUD complet reste possible via l'écran d'édition. + var existingOpt = repository.findByCode(dto.getCode()); + if (existingOpt.isPresent()) { + LOG.infof( + "Type d'organisation déjà existant pour le code %s, retour de l'entrée existante.", + dto.getCode()); + return toDTO(existingOpt.get()); + } + + TypeOrganisationEntity entity = new TypeOrganisationEntity(); + // métadonnées de création + entity.setCreePar(keycloakService.getCurrentUserEmail()); + applyToEntity(dto, entity); + repository.persist(entity); + return toDTO(entity); + } + + /** Met à jour un type existant. L'ID est utilisé comme identifiant principal. */ + @Transactional + public TypeOrganisationDTO update(UUID id, TypeOrganisationDTO dto) { + TypeOrganisationEntity entity = + repository + .findByIdOptional(id) + .orElseThrow(() -> new IllegalArgumentException("Type d'organisation introuvable")); + + if (dto.getCode() != null && !dto.getCode().equalsIgnoreCase(entity.getCode())) { + validateCode(dto.getCode()); + repository + .findByCode(dto.getCode()) + .ifPresent( + existing -> { + if (!existing.getId().equals(id)) { + throw new IllegalArgumentException( + "Un autre type d'organisation utilise déjà le code: " + dto.getCode()); + } + }); + entity.setCode(dto.getCode()); + } + + // métadonnées de modification + entity.setModifiePar(keycloakService.getCurrentUserEmail()); + applyToEntity(dto, entity); + repository.update(entity); + return toDTO(entity); + } + + /** Désactive logiquement un type. */ + @Transactional + public void disable(UUID id) { + TypeOrganisationEntity entity = + repository + .findByIdOptional(id) + .orElseThrow(() -> new IllegalArgumentException("Type d'organisation introuvable")); + entity.setActif(false); + repository.update(entity); + } + + private void validateCode(String code) { + if (code == null || code.trim().isEmpty()) { + throw new IllegalArgumentException("Le code du type d'organisation est obligatoire"); + } + // Plus aucune contrainte de format technique côté backend pour éviter les 400 inutiles. + // Le code est simplement normalisé en majuscules dans applyToEntity, ce qui suffit + // pour garantir la cohérence métier et la clé fonctionnelle. + } + + private TypeOrganisationDTO toDTO(TypeOrganisationEntity entity) { + TypeOrganisationDTO dto = new TypeOrganisationDTO(); + dto.setId(entity.getId()); + dto.setDateCreation(entity.getDateCreation()); + dto.setDateModification(entity.getDateModification()); + dto.setActif(entity.getActif()); + dto.setVersion(entity.getVersion()); + + dto.setCode(entity.getCode()); + dto.setLibelle(entity.getLibelle()); + dto.setDescription(entity.getDescription()); + dto.setOrdreAffichage(entity.getOrdreAffichage()); + return dto; + } + + private void applyToEntity(TypeOrganisationDTO dto, TypeOrganisationEntity entity) { + if (dto.getCode() != null) { + entity.setCode(dto.getCode().toUpperCase()); + } + if (dto.getLibelle() != null) { + entity.setLibelle(dto.getLibelle()); + } + entity.setDescription(dto.getDescription()); + entity.setOrdreAffichage(dto.getOrdreAffichage()); + if (dto.getActif() != null) { + entity.setActif(dto.getActif()); + } + } +} + + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/WaveService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/WaveService.java new file mode 100644 index 0000000..db63043 --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/WaveService.java @@ -0,0 +1,281 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.paiement.WaveBalanceDTO; +import dev.lions.unionflow.server.api.dto.paiement.WaveCheckoutSessionDTO; +import dev.lions.unionflow.server.api.dto.paiement.WaveWebhookDTO; +import dev.lions.unionflow.server.api.enums.paiement.StatutSession; +import dev.lions.unionflow.server.api.enums.paiement.TypeEvenement; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +/** + * Service pour l'intégration Wave Money + * Gère les sessions de paiement, les webhooks et la consultation du solde + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@ApplicationScoped +public class WaveService { + + private static final Logger LOG = Logger.getLogger(WaveService.class); + + @Inject + @ConfigProperty(name = "wave.api.key") + Optional waveApiKey; + + @Inject + @ConfigProperty(name = "wave.api.secret") + Optional waveApiSecret; + + @Inject + @ConfigProperty(name = "wave.api.base.url", defaultValue = "https://api.wave.com/v1") + String waveApiBaseUrl; + + @Inject + @ConfigProperty(name = "wave.environment", defaultValue = "sandbox") + String waveEnvironment; + + @Inject + @ConfigProperty(name = "wave.webhook.secret") + Optional waveWebhookSecret; + + /** + * Crée une session de paiement Wave Checkout + * + * @param montant Montant à payer + * @param devise Devise (XOF par défaut) + * @param successUrl URL de redirection en cas de succès + * @param errorUrl URL de redirection en cas d'erreur + * @param referenceUnionFlow Référence interne UnionFlow + * @param description Description du paiement + * @param organisationId ID de l'organisation + * @param membreId ID du membre + * @return Session de paiement créée + */ + @Transactional + public WaveCheckoutSessionDTO creerSessionPaiement( + BigDecimal montant, + String devise, + String successUrl, + String errorUrl, + String referenceUnionFlow, + String description, + UUID organisationId, + UUID membreId) { + LOG.infof( + "Création d'une session de paiement Wave: montant=%s, devise=%s, ref=%s", + montant, devise, referenceUnionFlow); + + try { + // TODO: Appel réel à l'API Wave Checkout + // Pour l'instant, simulation de la création de session + String waveSessionId = "wave_session_" + UUID.randomUUID().toString().replace("-", ""); + String waveUrl = buildWaveCheckoutUrl(waveSessionId); + + WaveCheckoutSessionDTO session = new WaveCheckoutSessionDTO(); + session.setId(UUID.randomUUID()); + session.setWaveSessionId(waveSessionId); + session.setWaveUrl(waveUrl); + session.setMontant(montant); + session.setDevise(devise != null ? devise : "XOF"); + session.setSuccessUrl(successUrl); + session.setErrorUrl(errorUrl); + session.setReferenceUnionFlow(referenceUnionFlow); + session.setDescription(description); + session.setOrganisationId(organisationId); + session.setMembreId(membreId); + session.setStatut(StatutSession.PENDING); + session.setDateCreation(LocalDateTime.now()); + session.setDateExpiration(LocalDateTime.now().plusHours(24)); // Expire dans 24h + + LOG.infof("Session Wave créée: %s", waveSessionId); + return session; + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création de la session Wave: %s", e.getMessage()); + throw new RuntimeException("Erreur lors de la création de la session Wave", e); + } + } + + /** + * Vérifie le statut d'une session de paiement + * + * @param waveSessionId ID de la session Wave + * @return Statut de la session + */ + public WaveCheckoutSessionDTO verifierStatutSession(String waveSessionId) { + LOG.infof("Vérification du statut de la session Wave: %s", waveSessionId); + + try { + // TODO: Appel réel à l'API Wave pour vérifier le statut + // Pour l'instant, simulation + WaveCheckoutSessionDTO session = new WaveCheckoutSessionDTO(); + session.setWaveSessionId(waveSessionId); + session.setStatut(StatutSession.PENDING); + return session; + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la vérification du statut: %s", e.getMessage()); + throw new RuntimeException("Erreur lors de la vérification du statut", e); + } + } + + /** + * Traite un webhook reçu de Wave + * + * @param payload Payload JSON du webhook + * @param signature Signature Wave pour vérification + * @param headers Headers HTTP + * @return Webhook traité + */ + @Transactional + public WaveWebhookDTO traiterWebhook(String payload, String signature, Map headers) { + LOG.info("Traitement d'un webhook Wave"); + + try { + // Vérifier la signature + if (!verifierSignatureWebhook(payload, signature)) { + LOG.warn("Signature webhook invalide"); + throw new SecurityException("Signature webhook invalide"); + } + + // Parser le payload + // TODO: Parser réellement le JSON du webhook + String webhookId = "webhook_" + UUID.randomUUID().toString(); + TypeEvenement typeEvenement = TypeEvenement.CHECKOUT_COMPLETE; // À déterminer depuis le payload + + WaveWebhookDTO webhook = new WaveWebhookDTO(); + webhook.setId(UUID.randomUUID()); + webhook.setWebhookId(webhookId); + webhook.setTypeEvenement(typeEvenement); + webhook.setCodeEvenement(typeEvenement.getCodeWave()); + webhook.setPayloadJson(payload); + webhook.setSignatureWave(signature); + webhook.setDateReception(LocalDateTime.now()); + webhook.setDateTraitement(LocalDateTime.now()); + + // Extraire les informations du payload + // TODO: Extraire réellement les données du JSON + // webhook.setSessionCheckoutId(...); + // webhook.setTransactionWaveId(...); + // webhook.setMontantTransaction(...); + + LOG.infof("Webhook traité: %s", webhookId); + return webhook; + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du traitement du webhook: %s", e.getMessage()); + throw new RuntimeException("Erreur lors du traitement du webhook", e); + } + } + + /** + * Consulte le solde du wallet Wave + * + * @return Solde du wallet + */ + public WaveBalanceDTO consulterSolde() { + LOG.info("Consultation du solde Wave"); + + try { + // TODO: Appel réel à l'API Wave Balance + // Pour l'instant, simulation + WaveBalanceDTO balance = new WaveBalanceDTO(); + balance.setNumeroWallet("wave_wallet_001"); + balance.setSoldeDisponible(BigDecimal.ZERO); + balance.setSoldeEnAttente(BigDecimal.ZERO); + balance.setSoldeTotal(BigDecimal.ZERO); + balance.setDevise("XOF"); + balance.setStatutWallet("ACTIVE"); + balance.setDateDerniereMiseAJour(LocalDateTime.now()); + balance.setDateDerniereSynchronisation(LocalDateTime.now()); + + return balance; + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la consultation du solde: %s", e.getMessage()); + throw new RuntimeException("Erreur lors de la consultation du solde", e); + } + } + + /** + * Vérifie si Wave est configuré et actif + * + * @return true si Wave est configuré + */ + public boolean estConfigure() { + return waveApiKey.isPresent() && !waveApiKey.get().isEmpty() + && waveApiSecret.isPresent() && !waveApiSecret.get().isEmpty(); + } + + /** + * Teste la connexion à l'API Wave + * + * @return Résultat du test + */ + public Map testerConnexion() { + LOG.info("Test de connexion à l'API Wave"); + + Map resultat = new HashMap<>(); + resultat.put("configure", estConfigure()); + resultat.put("environment", waveEnvironment); + resultat.put("baseUrl", waveApiBaseUrl); + resultat.put("timestamp", LocalDateTime.now().toString()); + + if (!estConfigure()) { + resultat.put("statut", "ERREUR"); + resultat.put("message", "Wave n'est pas configuré (clés API manquantes)"); + return resultat; + } + + try { + // TODO: Faire un appel réel à l'API Wave pour tester + resultat.put("statut", "OK"); + resultat.put("message", "Connexion réussie (simulation)"); + return resultat; + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du test de connexion: %s", e.getMessage()); + resultat.put("statut", "ERREUR"); + resultat.put("message", "Erreur: " + e.getMessage()); + return resultat; + } + } + + // Méthodes privées + + private String buildWaveCheckoutUrl(String sessionId) { + if ("sandbox".equals(waveEnvironment)) { + return "https://checkout-sandbox.wave.com/checkout/" + sessionId; + } else { + return "https://checkout.wave.com/checkout/" + sessionId; + } + } + + private boolean verifierSignatureWebhook(String payload, String signature) { + if (signature == null || signature.isEmpty()) { + return false; + } + + if (!waveWebhookSecret.isPresent() || waveWebhookSecret.get().isEmpty()) { + LOG.warn("Secret webhook non configuré, impossible de vérifier la signature"); + return false; + } + + // TODO: Implémenter la vérification réelle de la signature HMAC SHA256 + // La signature Wave est généralement au format: sha256= + return true; // Pour l'instant, on accepte toutes les signatures + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/resources/application.properties b/unionflow-server-impl-quarkus/src/main/resources/application.properties index f1bd171..b5df438 100644 --- a/unionflow-server-impl-quarkus/src/main/resources/application.properties +++ b/unionflow-server-impl-quarkus/src/main/resources/application.properties @@ -92,3 +92,10 @@ quarkus.log.category."io.quarkus".level=INFO # Configuration Jandex pour résoudre les warnings de réflexion quarkus.index-dependency.unionflow-server-api.group-id=dev.lions.unionflow quarkus.index-dependency.unionflow-server-api.artifact-id=unionflow-server-api + +# Configuration Wave Money +wave.api.key=${WAVE_API_KEY:} +wave.api.secret=${WAVE_API_SECRET:} +wave.api.base.url=${WAVE_API_BASE_URL:https://api.wave.com/v1} +wave.environment=${WAVE_ENVIRONMENT:sandbox} +wave.webhook.secret=${WAVE_WEBHOOK_SECRET:}