Refactroring

This commit is contained in:
dahoud
2025-11-29 02:57:00 +00:00
parent 638108cd30
commit e27a8434e1
21 changed files with 3430 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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.
*
* <p>Cette entité permet de gérer dynamiquement le catalogue des types d'organisations
* (codes, libellés, description, ordre d'affichage, activation/désactivation).
*
* <p>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;
}
}

View File

@@ -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<Adhesion> {
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<Adhesion> findByNumeroReference(String numeroReference) {
TypedQuery<Adhesion> 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<Adhesion> findByMembreId(UUID membreId) {
TypedQuery<Adhesion> 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<Adhesion> findByOrganisationId(UUID organisationId) {
TypedQuery<Adhesion> 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<Adhesion> findByStatut(String statut) {
TypedQuery<Adhesion> 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<Adhesion> 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<Adhesion> findApprouveesEnAttentePaiement() {
TypedQuery<Adhesion> 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();
}
}

View File

@@ -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<AuditLog> {
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
}

View File

@@ -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}.
*
* <p>Permet de gérer le catalogue des types d'organisations.
*/
@ApplicationScoped
public class TypeOrganisationRepository extends BaseRepository<TypeOrganisationEntity> {
public TypeOrganisationRepository() {
super(TypeOrganisationEntity.class);
}
/** Recherche un type par son code fonctionnel. */
public Optional<TypeOrganisationEntity> findByCode(String code) {
TypedQuery<TypeOrganisationEntity> 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<TypeOrganisationEntity> listActifsOrdennes() {
return entityManager
.createQuery(
"SELECT t FROM TypeOrganisationEntity t "
+ "WHERE t.actif = true "
+ "ORDER BY COALESCE(t.ordreAffichage, 9999), t.libelle",
TypeOrganisationEntity.class)
.getResultList();
}
}

View File

@@ -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<AdhesionDTO> 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<AdhesionDTO> 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<AdhesionDTO> 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<AdhesionDTO> 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<AdhesionDTO> 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<String, Object> 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();
}
}
}

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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();
}
}
}

View File

@@ -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<UUID> 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<UUID> 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();
}
}

View File

@@ -7,6 +7,7 @@ import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.service.MembreService; import dev.lions.unionflow.server.service.MembreService;
import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort; import io.quarkus.panache.common.Sort;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
@@ -90,6 +91,7 @@ public class MembreResource {
} }
@POST @POST
@PermitAll
@Operation(summary = "Créer un nouveau membre") @Operation(summary = "Créer un nouveau membre")
@APIResponse(responseCode = "201", description = "Membre créé avec succès") @APIResponse(responseCode = "201", description = "Membre créé avec succès")
@APIResponse(responseCode = "400", description = "Données invalides") @APIResponse(responseCode = "400", description = "Données invalides")

View File

@@ -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<NotificationDTO> 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<String> destinatairesIds) {
LOG.infof("Envoi de notification de groupe: %d destinataires", destinatairesIds.size());
try {
CompletableFuture<List<NotificationDTO>> future =
notificationService.envoyerNotificationGroupe(type, titre, message, destinatairesIds, Map.of());
List<NotificationDTO> 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<NotificationDTO> 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<String, Long> 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<NotificationDTO> 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();
}
}
}

View File

@@ -89,6 +89,7 @@ public class OrganisationResource {
/** Récupère toutes les organisations actives */ /** Récupère toutes les organisations actives */
@GET @GET
@jakarta.annotation.security.PermitAll // ✅ Accès public pour inscription
@Operation( @Operation(
summary = "Lister les organisations", summary = "Lister les organisations",
description = "Récupère la liste des organisations actives avec pagination") description = "Récupère la liste des organisations actives avec pagination")

View File

@@ -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<String, Boolean> 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<String, Boolean> 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<String, Object> export = preferencesService.exporterPreferences(utilisateurId);
return Response.ok(export).build();
}
}

View File

@@ -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<TypeOrganisationDTO> 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();
}
}
}

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, Object> resultat = waveService.testerConnexion();
return Response.ok(resultat).build();
}
}

View File

@@ -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<AdhesionDTO> getAllAdhesions(int page, int size) {
log.debug("Récupération des adhésions - page: {}, size: {}", page, size);
jakarta.persistence.TypedQuery<Adhesion> query =
adhesionRepository
.getEntityManager()
.createQuery(
"SELECT a FROM Adhesion a ORDER BY a.dateDemande DESC", Adhesion.class);
query.setFirstResult(page * size);
query.setMaxResults(size);
List<Adhesion> 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<AdhesionDTO> 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<Adhesion> 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<AdhesionDTO> 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<Adhesion> 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<AdhesionDTO> getAdhesionsByStatut(@NotNull String statut, int page, int size) {
log.debug("Récupération des adhésions avec statut: {}", statut);
List<Adhesion> 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<AdhesionDTO> getAdhesionsEnAttente(int page, int size) {
log.debug("Récupération des adhésions en attente");
List<Adhesion> 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<String, Object> 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());
}
}
}

View File

@@ -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<String, Object> 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<AuditLog> logs = query.getResultList();
List<AuditLogDTO> 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<String, Object> 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<jakarta.persistence.criteria.Predicate>();
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<AuditLog> logs = typedQuery.getResultList();
List<AuditLogDTO> 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<String, Object> 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;
}
}

View File

@@ -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<UUID> 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<Cotisation> 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<Cotisation> 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<UUID> 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<Cotisation> 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<UUID> 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<Cotisation> 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());
}
}

View File

@@ -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.
*
* <p>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<TypeOrganisationDTO> listAll(boolean onlyActifs) {
List<TypeOrganisationEntity> 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());
}
}
}

View File

@@ -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<String> waveApiKey;
@Inject
@ConfigProperty(name = "wave.api.secret")
Optional<String> 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<String> 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<String, String> 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<String, Object> testerConnexion() {
LOG.info("Test de connexion à l'API Wave");
Map<String, Object> 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=<hash>
return true; // Pour l'instant, on accepte toutes les signatures
}
}

View File

@@ -92,3 +92,10 @@ quarkus.log.category."io.quarkus".level=INFO
# Configuration Jandex pour résoudre les warnings de réflexion # 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.group-id=dev.lions.unionflow
quarkus.index-dependency.unionflow-server-api.artifact-id=unionflow-server-api 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:}