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