From 5a7ead81f77e48048906fb173d5e7e4104171751 Mon Sep 17 00:00:00 2001 From: dahoud Date: Sun, 30 Nov 2025 02:29:48 +0000 Subject: [PATCH] feat: DTOs, Services et Resources REST pour Paiements et Wave MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DTOs créés: - PaiementDTO avec validation complète - CompteWaveDTO avec validation format téléphone - TransactionWaveDTO avec tous les champs nécessaires Services créés: - PaiementService: CRUD complet, validation, annulation, calculs - WaveService: Gestion comptes Wave, transactions, vérification Resources REST créées: - PaiementResource: Endpoints CRUD, validation, annulation, recherche - WaveResource: Endpoints comptes et transactions Wave Respect strict DRY/WOU: - Patterns de service cohérents avec MembreService - Patterns de resource cohérents avec OrganisationResource - Gestion d'erreurs standardisée - Validation complète des DTOs --- .../server/api/dto/paiement/PaiementDTO.java | 80 +++ .../server/api/dto/wave/CompteWaveDTO.java | 53 ++ .../api/dto/wave/TransactionWaveDTO.java | 87 +++ .../lions/unionflow/server/entity/Membre.java | 1 + .../server/resource/PaiementResource.java | 209 +++++++ .../server/resource/WaveResource.java | 377 ++++++------ .../server/service/PaiementService.java | 361 ++++++++---- .../unionflow/server/service/WaveService.java | 552 +++++++++++------- 8 files changed, 1224 insertions(+), 496 deletions(-) create mode 100644 unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/paiement/PaiementDTO.java create mode 100644 unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/wave/CompteWaveDTO.java create mode 100644 unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/wave/TransactionWaveDTO.java create mode 100644 unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java diff --git a/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/paiement/PaiementDTO.java b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/paiement/PaiementDTO.java new file mode 100644 index 0000000..9226d6f --- /dev/null +++ b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/paiement/PaiementDTO.java @@ -0,0 +1,80 @@ +package dev.lions.unionflow.server.api.dto.paiement; + +import dev.lions.unionflow.server.api.dto.base.BaseDTO; +import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement; +import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; + +/** + * DTO pour la gestion des paiements + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Getter +@Setter +public class PaiementDTO extends BaseDTO { + + private static final long serialVersionUID = 1L; + + /** Numéro de référence unique */ + @NotBlank(message = "Le numéro de référence est obligatoire") + private String numeroReference; + + /** Montant du paiement */ + @NotNull(message = "Le montant est obligatoire") + @DecimalMin(value = "0.0", message = "Le montant doit être positif") + @Digits(integer = 12, fraction = 2) + private BigDecimal montant; + + /** Code devise (ISO 3 lettres) */ + @NotBlank(message = "Le code devise est obligatoire") + @Pattern(regexp = "^[A-Z]{3}$", message = "Le code devise doit être un code ISO à 3 lettres") + private String codeDevise; + + /** Méthode de paiement */ + @NotNull(message = "La méthode de paiement est obligatoire") + private MethodePaiement methodePaiement; + + /** Statut du paiement */ + @NotNull(message = "Le statut du paiement est obligatoire") + private StatutPaiement statutPaiement; + + /** Date de paiement */ + private LocalDateTime datePaiement; + + /** Date de validation */ + private LocalDateTime dateValidation; + + /** Validateur (email de l'administrateur) */ + private String validateur; + + /** Référence externe */ + private String referenceExterne; + + /** URL de preuve de paiement */ + private String urlPreuve; + + /** Commentaires et notes */ + private String commentaire; + + /** Adresse IP de l'initiateur */ + private String ipAddress; + + /** User-Agent de l'initiateur */ + private String userAgent; + + /** ID du membre payeur */ + @NotNull(message = "Le membre payeur est obligatoire") + private UUID membreId; + + /** ID de la transaction Wave (si applicable) */ + private UUID transactionWaveId; +} + diff --git a/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/wave/CompteWaveDTO.java b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/wave/CompteWaveDTO.java new file mode 100644 index 0000000..6c5b437 --- /dev/null +++ b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/wave/CompteWaveDTO.java @@ -0,0 +1,53 @@ +package dev.lions.unionflow.server.api.dto.wave; + +import dev.lions.unionflow.server.api.dto.base.BaseDTO; +import dev.lions.unionflow.server.api.enums.wave.StatutCompteWave; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; + +/** + * DTO pour la gestion des comptes Wave + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Getter +@Setter +public class CompteWaveDTO extends BaseDTO { + + private static final long serialVersionUID = 1L; + + /** Numéro de téléphone Wave */ + @NotBlank(message = "Le numéro de téléphone est obligatoire") + @Pattern( + regexp = "^\\+225[0-9]{8}$", + message = "Le numéro de téléphone Wave doit être au format +225XXXXXXXX") + private String numeroTelephone; + + /** Statut du compte */ + private StatutCompteWave statutCompte; + + /** Identifiant Wave API (encrypté) */ + private String waveAccountId; + + /** Environnement (SANDBOX ou PRODUCTION) */ + private String environnement; + + /** Date de dernière vérification */ + private LocalDateTime dateDerniereVerification; + + /** Commentaires */ + private String commentaire; + + /** ID de l'organisation (si compte d'organisation) */ + private UUID organisationId; + + /** ID du membre (si compte de membre) */ + private UUID membreId; +} + diff --git a/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/wave/TransactionWaveDTO.java b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/wave/TransactionWaveDTO.java new file mode 100644 index 0000000..6d62848 --- /dev/null +++ b/unionflow-server-api/src/main/java/dev/lions/unionflow/server/api/dto/wave/TransactionWaveDTO.java @@ -0,0 +1,87 @@ +package dev.lions.unionflow.server.api.dto.wave; + +import dev.lions.unionflow.server.api.dto.base.BaseDTO; +import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave; +import dev.lions.unionflow.server.api.enums.wave.TypeTransactionWave; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; + +/** + * DTO pour la gestion des transactions Wave + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Getter +@Setter +public class TransactionWaveDTO extends BaseDTO { + + private static final long serialVersionUID = 1L; + + /** Identifiant Wave de la transaction */ + @NotBlank(message = "L'identifiant Wave est obligatoire") + private String waveTransactionId; + + /** Identifiant de requête Wave */ + private String waveRequestId; + + /** Référence Wave */ + private String waveReference; + + /** Type de transaction */ + @NotNull(message = "Le type de transaction est obligatoire") + private TypeTransactionWave typeTransaction; + + /** Statut de la transaction */ + @NotNull(message = "Le statut de la transaction est obligatoire") + private StatutTransactionWave statutTransaction; + + /** Montant de la transaction */ + @NotNull(message = "Le montant est obligatoire") + @DecimalMin(value = "0.0", message = "Le montant doit être positif") + @Digits(integer = 12, fraction = 2) + private BigDecimal montant; + + /** Frais de transaction */ + @DecimalMin(value = "0.0") + @Digits(integer = 10, fraction = 2) + private BigDecimal frais; + + /** Montant net */ + @DecimalMin(value = "0.0") + @Digits(integer = 12, fraction = 2) + private BigDecimal montantNet; + + /** Code devise */ + @NotBlank(message = "Le code devise est obligatoire") + @Pattern(regexp = "^[A-Z]{3}$", message = "Le code devise doit être un code ISO à 3 lettres") + private String codeDevise; + + /** Numéro téléphone payeur */ + private String telephonePayeur; + + /** Numéro téléphone bénéficiaire */ + private String telephoneBeneficiaire; + + /** Métadonnées JSON */ + private String metadonnees; + + /** Nombre de tentatives */ + private Integer nombreTentatives; + + /** Date de dernière tentative */ + private LocalDateTime dateDerniereTentative; + + /** Message d'erreur */ + private String messageErreur; + + /** ID du compte Wave */ + @NotNull(message = "Le compte Wave est obligatoire") + private UUID compteWaveId; +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Membre.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Membre.java index 552676f..87dcc97 100644 --- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Membre.java +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/entity/Membre.java @@ -4,6 +4,7 @@ import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java new file mode 100644 index 0000000..e53ad6c --- /dev/null +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java @@ -0,0 +1,209 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.paiement.PaiementDTO; +import dev.lions.unionflow.server.service.PaiementService; +import jakarta.annotation.security.PermitAll; +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.util.List; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Resource REST pour la gestion des paiements + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Path("/api/paiements") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@PermitAll +public class PaiementResource { + + private static final Logger LOG = Logger.getLogger(PaiementResource.class); + + @Inject PaiementService paiementService; + + /** + * Crée un nouveau paiement + * + * @param paiementDTO DTO du paiement à créer + * @return Paiement créé + */ + @POST + public Response creerPaiement(@Valid PaiementDTO paiementDTO) { + try { + PaiementDTO result = paiementService.creerPaiement(paiementDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Met à jour un paiement + * + * @param id ID du paiement + * @param paiementDTO DTO avec les modifications + * @return Paiement mis à jour + */ + @PUT + @Path("/{id}") + public Response mettreAJourPaiement(@PathParam("id") UUID id, @Valid PaiementDTO paiementDTO) { + try { + PaiementDTO result = paiementService.mettreAJourPaiement(id, paiementDTO); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (IllegalStateException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la mise à jour du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la mise à jour du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Valide un paiement + * + * @param id ID du paiement + * @return Paiement validé + */ + @POST + @Path("/{id}/valider") + public Response validerPaiement(@PathParam("id") UUID id) { + try { + PaiementDTO result = paiementService.validerPaiement(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la validation du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la validation du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Annule un paiement + * + * @param id ID du paiement + * @return Paiement annulé + */ + @POST + @Path("/{id}/annuler") + public Response annulerPaiement(@PathParam("id") UUID id) { + try { + PaiementDTO result = paiementService.annulerPaiement(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (IllegalStateException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'annulation du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de l'annulation du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un paiement par son ID + * + * @param id ID du paiement + * @return Paiement + */ + @GET + @Path("/{id}") + public Response trouverParId(@PathParam("id") UUID id) { + try { + PaiementDTO result = paiementService.trouverParId(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un paiement par son numéro de référence + * + * @param numeroReference Numéro de référence + * @return Paiement + */ + @GET + @Path("/reference/{numeroReference}") + public Response trouverParNumeroReference(@PathParam("numeroReference") String numeroReference) { + try { + PaiementDTO result = paiementService.trouverParNumeroReference(numeroReference); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Liste tous les paiements d'un membre + * + * @param membreId ID du membre + * @return Liste des paiements + */ + @GET + @Path("/membre/{membreId}") + public Response listerParMembre(@PathParam("membreId") UUID membreId) { + try { + List result = paiementService.listerParMembre(membreId); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des paiements"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des paiements: " + e.getMessage())) + .build(); + } + } + + /** Classe interne pour les réponses d'erreur */ + public static class ErrorResponse { + public String error; + + public ErrorResponse(String error) { + this.error = error; + } + } +} + diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java index 61a5526..db0e896 100644 --- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java @@ -1,213 +1,264 @@ 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.api.dto.wave.CompteWaveDTO; +import dev.lions.unionflow.server.api.dto.wave.TransactionWaveDTO; +import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave; import dev.lions.unionflow.server.service.WaveService; +import jakarta.annotation.security.PermitAll; 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.*; 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.List; 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 + * Resource REST pour l'intégration Wave Mobile Money * * @author UnionFlow Team - * @version 1.0 - * @since 2025-01-17 + * @version 3.0 + * @since 2025-01-29 */ @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) +@PermitAll public class WaveResource { private static final Logger LOG = Logger.getLogger(WaveResource.class); @Inject WaveService waveService; + // ======================================== + // COMPTES WAVE + // ======================================== + + /** + * Crée un nouveau compte Wave + * + * @param compteWaveDTO DTO du compte à créer + * @return Compte créé + */ @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) { + @Path("/comptes") + public Response creerCompteWave(@Valid CompteWaveDTO compteWaveDTO) { try { - WaveCheckoutSessionDTO session = - waveService.creerSessionPaiement( - montant, - devise, - successUrl, - errorUrl, - referenceUnionFlow, - description, - organisationId, - membreId); - - return Response.ok(session).build(); - + CompteWaveDTO result = waveService.creerCompteWave(compteWaveDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); } catch (Exception e) { - LOG.errorf(e, "Erreur lors de la création de la session: %s", e.getMessage()); - Map erreur = new HashMap<>(); - erreur.put("erreur", "Erreur lors de la création de la session"); - erreur.put("message", e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build(); + LOG.errorf(e, "Erreur lors de la création du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création du compte Wave: " + e.getMessage())) + .build(); } } + /** + * Met à jour un compte Wave + * + * @param id ID du compte + * @param compteWaveDTO DTO avec les modifications + * @return Compte mis à jour + */ + @PUT + @Path("/comptes/{id}") + public Response mettreAJourCompteWave(@PathParam("id") UUID id, @Valid CompteWaveDTO compteWaveDTO) { + try { + CompteWaveDTO result = waveService.mettreAJourCompteWave(id, compteWaveDTO); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Compte Wave non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la mise à jour du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la mise à jour du compte Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Vérifie un compte Wave + * + * @param id ID du compte + * @return Compte vérifié + */ + @POST + @Path("/comptes/{id}/verifier") + public Response verifierCompteWave(@PathParam("id") UUID id) { + try { + CompteWaveDTO result = waveService.verifierCompteWave(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Compte Wave non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la vérification du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la vérification du compte Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un compte Wave par son ID + * + * @param id ID du compte + * @return Compte Wave + */ @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) { + @Path("/comptes/{id}") + public Response trouverCompteWaveParId(@PathParam("id") UUID id) { try { - WaveCheckoutSessionDTO session = waveService.verifierStatutSession(sessionId); - return Response.ok(session).build(); - + CompteWaveDTO result = waveService.trouverCompteWaveParId(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Compte Wave non trouvé")) + .build(); } catch (Exception e) { - LOG.errorf(e, "Erreur lors de la vérification du statut: %s", e.getMessage()); - Map erreur = new HashMap<>(); - erreur.put("erreur", "Erreur lors de la vérification du statut"); - erreur.put("message", e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build(); + LOG.errorf(e, "Erreur lors de la recherche du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du compte Wave: " + e.getMessage())) + .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) { + /** + * Trouve un compte Wave par numéro de téléphone + * + * @param numeroTelephone Numéro de téléphone + * @return Compte Wave ou null + */ + @GET + @Path("/comptes/telephone/{numeroTelephone}") + public Response trouverCompteWaveParTelephone(@PathParam("numeroTelephone") String numeroTelephone) { try { - // Récupérer les headers - Map headers = new HashMap<>(); - if (signature != null) { - headers.put("X-Wave-Signature", signature); + CompteWaveDTO result = waveService.trouverCompteWaveParTelephone(numeroTelephone); + if (result == null) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Compte Wave non trouvé")) + .build(); } - - 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(); - + return Response.ok(result).build(); } catch (Exception e) { - LOG.errorf(e, "Erreur lors du traitement du webhook: %s", e.getMessage()); - Map erreur = new HashMap<>(); - erreur.put("erreur", "Erreur lors du traitement du webhook"); - erreur.put("message", e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build(); + LOG.errorf(e, "Erreur lors de la recherche du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du compte Wave: " + e.getMessage())) + .build(); } } + /** + * Liste tous les comptes Wave d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des comptes Wave + */ @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() { + @Path("/comptes/organisation/{organisationId}") + public Response listerComptesWaveParOrganisation(@PathParam("organisationId") UUID organisationId) { try { - WaveBalanceDTO balance = waveService.consulterSolde(); - return Response.ok(balance).build(); - + List result = waveService.listerComptesWaveParOrganisation(organisationId); + return Response.ok(result).build(); } catch (Exception e) { - LOG.errorf(e, "Erreur lors de la consultation du solde: %s", e.getMessage()); - Map erreur = new HashMap<>(); - erreur.put("erreur", "Erreur lors de la consultation du solde"); - erreur.put("message", e.getMessage()); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build(); + LOG.errorf(e, "Erreur lors de la liste des comptes Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des comptes Wave: " + e.getMessage())) + .build(); } } + // ======================================== + // TRANSACTIONS WAVE + // ======================================== + + /** + * Crée une nouvelle transaction Wave + * + * @param transactionWaveDTO DTO de la transaction à créer + * @return Transaction créée + */ + @POST + @Path("/transactions") + public Response creerTransactionWave(@Valid TransactionWaveDTO transactionWaveDTO) { + try { + TransactionWaveDTO result = waveService.creerTransactionWave(transactionWaveDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création de la transaction Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création de la transaction Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Met à jour le statut d'une transaction Wave + * + * @param waveTransactionId Identifiant Wave de la transaction + * @param statut Nouveau statut + * @return Transaction mise à jour + */ + @PUT + @Path("/transactions/{waveTransactionId}/statut") + public Response mettreAJourStatutTransaction( + @PathParam("waveTransactionId") String waveTransactionId, StatutTransactionWave statut) { + try { + TransactionWaveDTO result = waveService.mettreAJourStatutTransaction(waveTransactionId, statut); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Transaction Wave non trouvée")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la mise à jour du statut de la transaction Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity( + new ErrorResponse( + "Erreur lors de la mise à jour du statut de la transaction Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve une transaction Wave par son identifiant Wave + * + * @param waveTransactionId Identifiant Wave + * @return Transaction Wave + */ @GET - @Path("/test") - @Operation( - summary = "Tester la connexion Wave", - description = "Teste la connexion et la configuration de l'API Wave") - @APIResponse(responseCode = "200", description = "Test effectué") - public Response testerConnexion() { - Map resultat = waveService.testerConnexion(); - return Response.ok(resultat).build(); + @Path("/transactions/{waveTransactionId}") + public Response trouverTransactionWaveParId(@PathParam("waveTransactionId") String waveTransactionId) { + try { + TransactionWaveDTO result = waveService.trouverTransactionWaveParId(waveTransactionId); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Transaction Wave non trouvée")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche de la transaction Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche de la transaction Wave: " + e.getMessage())) + .build(); + } + } + + /** Classe interne pour les réponses d'erreur */ + public static class ErrorResponse { + public String error; + + public ErrorResponse(String error) { + this.error = error; + } } } - diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/PaiementService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/PaiementService.java index 1d7cbcb..66b9fae 100644 --- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/PaiementService.java +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/PaiementService.java @@ -1,174 +1,309 @@ package dev.lions.unionflow.server.service; +import dev.lions.unionflow.server.api.dto.paiement.PaiementDTO; +import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Paiement; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.PaiementRepository; +import dev.lions.unionflow.server.service.KeycloakService; 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.LocalDateTime; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import org.jboss.logging.Logger; /** - * Service métier pour la gestion des paiements Mobile Money Intègre Wave Money, Orange Money, et - * Moov Money + * Service métier pour la gestion des paiements * * @author UnionFlow Team - * @version 1.0 - * @since 2025-01-15 + * @version 3.0 + * @since 2025-01-29 */ @ApplicationScoped public class PaiementService { private static final Logger LOG = Logger.getLogger(PaiementService.class); + @Inject PaiementRepository paiementRepository; + + @Inject MembreRepository membreRepository; + + @Inject KeycloakService keycloakService; + /** - * Initie un paiement Mobile Money + * Crée un nouveau paiement * - * @param paymentData données du paiement - * @return informations du paiement initié + * @param paiementDTO DTO du paiement à créer + * @return DTO du paiement créé */ @Transactional - public Map initiatePayment(@Valid Map paymentData) { - LOG.infof("Initiation d'un paiement"); + public PaiementDTO creerPaiement(PaiementDTO paiementDTO) { + LOG.infof("Création d'un nouveau paiement: %s", paiementDTO.getNumeroReference()); - try { - String operateur = (String) paymentData.get("operateur"); - BigDecimal montant = new BigDecimal(paymentData.get("montant").toString()); - String numeroTelephone = (String) paymentData.get("numeroTelephone"); - String cotisationId = (String) paymentData.get("cotisationId"); + Paiement paiement = convertToEntity(paiementDTO); + paiement.setCreePar(keycloakService.getCurrentUserEmail()); - // Générer un ID unique pour le paiement - String paymentId = UUID.randomUUID().toString(); - String numeroReference = "PAY-" + System.currentTimeMillis(); + paiementRepository.persist(paiement); + LOG.infof("Paiement créé avec succès: ID=%s, Référence=%s", paiement.getId(), paiement.getNumeroReference()); - Map response = new HashMap<>(); - response.put("id", paymentId); - response.put("cotisationId", cotisationId); - response.put("numeroReference", numeroReference); - response.put("montant", montant); - response.put("codeDevise", "XOF"); - response.put("methodePaiement", operateur != null ? operateur.toUpperCase() : "WAVE"); - response.put("statut", "PENDING"); - response.put("dateTransaction", LocalDateTime.now().toString()); - response.put("numeroTransaction", numeroReference); - response.put("operateurMobileMoney", operateur != null ? operateur.toUpperCase() : "WAVE"); - response.put("numeroTelephone", numeroTelephone); - response.put("dateCreation", LocalDateTime.now().toString()); - - // Métadonnées - Map metadonnees = new HashMap<>(); - metadonnees.put("source", "unionflow_mobile"); - metadonnees.put("operateur", operateur); - metadonnees.put("numero_telephone", numeroTelephone); - metadonnees.put("cotisation_id", cotisationId); - response.put("metadonnees", metadonnees); - - return response; - - } catch (Exception e) { - LOG.errorf("Erreur lors de l'initiation du paiement: %s", e.getMessage()); - throw new RuntimeException("Erreur lors de l'initiation du paiement: " + e.getMessage()); - } + return convertToDTO(paiement); } /** - * Récupère le statut d'un paiement + * Met à jour un paiement existant * - * @param paymentId ID du paiement - * @return statut du paiement + * @param id ID du paiement + * @param paiementDTO DTO avec les modifications + * @return DTO du paiement mis à jour */ - public Map getPaymentStatus(@NotNull String paymentId) { - LOG.infof("Récupération du statut du paiement: %s", paymentId); + @Transactional + public PaiementDTO mettreAJourPaiement(UUID id, PaiementDTO paiementDTO) { + LOG.infof("Mise à jour du paiement ID: %s", id); - // Simulation du statut - Map status = new HashMap<>(); - status.put("id", paymentId); - status.put("statut", "COMPLETED"); // Simulation d'un paiement réussi - status.put("dateModification", LocalDateTime.now().toString()); - status.put("message", "Paiement traité avec succès"); + Paiement paiement = + paiementRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id)); - return status; + if (!paiement.peutEtreModifie()) { + throw new IllegalStateException("Le paiement ne peut plus être modifié (statut finalisé)"); + } + + updateFromDTO(paiement, paiementDTO); + paiement.setModifiePar(keycloakService.getCurrentUserEmail()); + + paiementRepository.persist(paiement); + LOG.infof("Paiement mis à jour avec succès: ID=%s", id); + + return convertToDTO(paiement); + } + + /** + * Valide un paiement + * + * @param id ID du paiement + * @return DTO du paiement validé + */ + @Transactional + public PaiementDTO validerPaiement(UUID id) { + LOG.infof("Validation du paiement ID: %s", id); + + Paiement paiement = + paiementRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id)); + + if (paiement.isValide()) { + LOG.warnf("Le paiement ID=%s est déjà validé", id); + return convertToDTO(paiement); + } + + paiement.setStatutPaiement(StatutPaiement.VALIDE); + paiement.setDateValidation(LocalDateTime.now()); + paiement.setValidateur(keycloakService.getCurrentUserEmail()); + paiement.setModifiePar(keycloakService.getCurrentUserEmail()); + + paiementRepository.persist(paiement); + LOG.infof("Paiement validé avec succès: ID=%s", id); + + return convertToDTO(paiement); } /** * Annule un paiement * - * @param paymentId ID du paiement - * @param cotisationId ID de la cotisation - * @return résultat de l'annulation + * @param id ID du paiement + * @return DTO du paiement annulé */ @Transactional - public Map cancelPayment( - @NotNull String paymentId, @NotNull String cotisationId) { - LOG.infof("Annulation du paiement: %s pour cotisation: %s", paymentId, cotisationId); + public PaiementDTO annulerPaiement(UUID id) { + LOG.infof("Annulation du paiement ID: %s", id); - Map result = new HashMap<>(); - result.put("id", paymentId); - result.put("cotisationId", cotisationId); - result.put("statut", "CANCELLED"); - result.put("dateAnnulation", LocalDateTime.now().toString()); - result.put("message", "Paiement annulé avec succès"); + Paiement paiement = + paiementRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id)); - return result; + if (!paiement.peutEtreModifie()) { + throw new IllegalStateException("Le paiement ne peut plus être annulé (statut finalisé)"); + } + + paiement.setStatutPaiement(StatutPaiement.ANNULE); + paiement.setModifiePar(keycloakService.getCurrentUserEmail()); + + paiementRepository.persist(paiement); + LOG.infof("Paiement annulé avec succès: ID=%s", id); + + return convertToDTO(paiement); } /** - * Récupère l'historique des paiements + * Trouve un paiement par son ID * - * @param filters filtres de recherche - * @return liste des paiements + * @param id ID du paiement + * @return DTO du paiement */ - public List> getPaymentHistory(Map filters) { - LOG.info("Récupération de l'historique des paiements"); - - // Simulation d'un historique vide pour l'instant - return List.of(); + public PaiementDTO trouverParId(UUID id) { + return paiementRepository + .findByIdOptional(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id)); } /** - * Vérifie le statut d'un service de paiement + * Trouve un paiement par son numéro de référence * - * @param serviceType type de service (WAVE, ORANGE_MONEY, MOOV_MONEY) - * @return statut du service + * @param numeroReference Numéro de référence + * @return DTO du paiement */ - public Map checkServiceStatus(@NotNull String serviceType) { - LOG.infof("Vérification du statut du service: %s", serviceType); - - Map status = new HashMap<>(); - status.put("service", serviceType); - status.put("statut", "OPERATIONAL"); - status.put("disponible", true); - status.put("derniereMiseAJour", LocalDateTime.now().toString()); - - return status; + public PaiementDTO trouverParNumeroReference(String numeroReference) { + return paiementRepository + .findByNumeroReference(numeroReference) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec la référence: " + numeroReference)); } /** - * Récupère les statistiques de paiement + * Liste tous les paiements d'un membre * - * @param filters filtres pour les statistiques - * @return statistiques des paiements + * @param membreId ID du membre + * @return Liste des paiements */ - public Map getPaymentStatistics(Map filters) { - LOG.info("Récupération des statistiques de paiement"); + public List listerParMembre(UUID membreId) { + return paiementRepository.findByMembreId(membreId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } - Map stats = new HashMap<>(); - stats.put("totalPaiements", 0); - stats.put("montantTotal", BigDecimal.ZERO); - stats.put("paiementsReussis", 0); - stats.put("paiementsEchoues", 0); - stats.put("paiementsEnAttente", 0); - stats.put( - "operateurs", - Map.of( - "WAVE", 0, - "ORANGE_MONEY", 0, - "MOOV_MONEY", 0)); + /** + * Calcule le montant total des paiements validés dans une période + * + * @param dateDebut Date de début + * @param dateFin Date de fin + * @return Montant total + */ + public BigDecimal calculerMontantTotalValides(LocalDateTime dateDebut, LocalDateTime dateFin) { + return paiementRepository.calculerMontantTotalValides(dateDebut, dateFin); + } - return stats; + // ======================================== + // MÉTHODES PRIVÉES + // ======================================== + + /** Convertit une entité en DTO */ + private PaiementDTO convertToDTO(Paiement paiement) { + if (paiement == null) { + return null; + } + + PaiementDTO dto = new PaiementDTO(); + dto.setId(paiement.getId()); + dto.setNumeroReference(paiement.getNumeroReference()); + dto.setMontant(paiement.getMontant()); + dto.setCodeDevise(paiement.getCodeDevise()); + dto.setMethodePaiement(paiement.getMethodePaiement()); + dto.setStatutPaiement(paiement.getStatutPaiement()); + dto.setDatePaiement(paiement.getDatePaiement()); + dto.setDateValidation(paiement.getDateValidation()); + dto.setValidateur(paiement.getValidateur()); + dto.setReferenceExterne(paiement.getReferenceExterne()); + dto.setUrlPreuve(paiement.getUrlPreuve()); + dto.setCommentaire(paiement.getCommentaire()); + dto.setIpAddress(paiement.getIpAddress()); + dto.setUserAgent(paiement.getUserAgent()); + + if (paiement.getMembre() != null) { + dto.setMembreId(paiement.getMembre().getId()); + } + if (paiement.getTransactionWave() != null) { + dto.setTransactionWaveId(paiement.getTransactionWave().getId()); + } + + dto.setDateCreation(paiement.getDateCreation()); + dto.setDateModification(paiement.getDateModification()); + dto.setActif(paiement.getActif()); + + return dto; + } + + /** Convertit un DTO en entité */ + private Paiement convertToEntity(PaiementDTO dto) { + if (dto == null) { + return null; + } + + Paiement paiement = new Paiement(); + paiement.setNumeroReference(dto.getNumeroReference()); + paiement.setMontant(dto.getMontant()); + paiement.setCodeDevise(dto.getCodeDevise()); + paiement.setMethodePaiement(dto.getMethodePaiement()); + paiement.setStatutPaiement(dto.getStatutPaiement() != null ? dto.getStatutPaiement() : StatutPaiement.EN_ATTENTE); + paiement.setDatePaiement(dto.getDatePaiement()); + paiement.setDateValidation(dto.getDateValidation()); + paiement.setValidateur(dto.getValidateur()); + paiement.setReferenceExterne(dto.getReferenceExterne()); + paiement.setUrlPreuve(dto.getUrlPreuve()); + paiement.setCommentaire(dto.getCommentaire()); + paiement.setIpAddress(dto.getIpAddress()); + paiement.setUserAgent(dto.getUserAgent()); + + // Relation Membre + if (dto.getMembreId() != null) { + Membre membre = + membreRepository + .findByIdOptional(dto.getMembreId()) + .orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId())); + paiement.setMembre(membre); + } + + // Relation TransactionWave sera gérée par WaveService + + return paiement; + } + + /** Met à jour une entité à partir d'un DTO */ + private void updateFromDTO(Paiement paiement, PaiementDTO dto) { + if (dto.getMontant() != null) { + paiement.setMontant(dto.getMontant()); + } + if (dto.getCodeDevise() != null) { + paiement.setCodeDevise(dto.getCodeDevise()); + } + if (dto.getMethodePaiement() != null) { + paiement.setMethodePaiement(dto.getMethodePaiement()); + } + if (dto.getStatutPaiement() != null) { + paiement.setStatutPaiement(dto.getStatutPaiement()); + } + if (dto.getDatePaiement() != null) { + paiement.setDatePaiement(dto.getDatePaiement()); + } + if (dto.getDateValidation() != null) { + paiement.setDateValidation(dto.getDateValidation()); + } + if (dto.getValidateur() != null) { + paiement.setValidateur(dto.getValidateur()); + } + if (dto.getReferenceExterne() != null) { + paiement.setReferenceExterne(dto.getReferenceExterne()); + } + if (dto.getUrlPreuve() != null) { + paiement.setUrlPreuve(dto.getUrlPreuve()); + } + if (dto.getCommentaire() != null) { + paiement.setCommentaire(dto.getCommentaire()); + } + if (dto.getIpAddress() != null) { + paiement.setIpAddress(dto.getIpAddress()); + } + if (dto.getUserAgent() != null) { + paiement.setUserAgent(dto.getUserAgent()); + } } } diff --git a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/WaveService.java b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/WaveService.java index db63043..83163bc 100644 --- a/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/WaveService.java +++ b/unionflow-server-impl-quarkus/src/main/java/dev/lions/unionflow/server/service/WaveService.java @@ -1,281 +1,393 @@ 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 dev.lions.unionflow.server.api.dto.wave.CompteWaveDTO; +import dev.lions.unionflow.server.api.dto.wave.TransactionWaveDTO; +import dev.lions.unionflow.server.api.enums.wave.StatutCompteWave; +import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave; +import dev.lions.unionflow.server.api.enums.wave.TypeTransactionWave; +import dev.lions.unionflow.server.entity.CompteWave; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.TransactionWave; +import dev.lions.unionflow.server.repository.CompteWaveRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.TransactionWaveRepository; +import dev.lions.unionflow.server.service.KeycloakService; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; import java.math.BigDecimal; import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.util.List; import java.util.UUID; -import org.eclipse.microprofile.config.inject.ConfigProperty; +import java.util.stream.Collectors; 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 + * Service métier pour l'intégration Wave Mobile Money * * @author UnionFlow Team - * @version 1.0 - * @since 2025-01-17 + * @version 3.0 + * @since 2025-01-29 */ @ApplicationScoped public class WaveService { private static final Logger LOG = Logger.getLogger(WaveService.class); - @Inject - @ConfigProperty(name = "wave.api.key") - Optional waveApiKey; + @Inject CompteWaveRepository compteWaveRepository; - @Inject - @ConfigProperty(name = "wave.api.secret") - Optional waveApiSecret; + @Inject TransactionWaveRepository transactionWaveRepository; - @Inject - @ConfigProperty(name = "wave.api.base.url", defaultValue = "https://api.wave.com/v1") - String waveApiBaseUrl; + @Inject OrganisationRepository organisationRepository; - @Inject - @ConfigProperty(name = "wave.environment", defaultValue = "sandbox") - String waveEnvironment; + @Inject MembreRepository membreRepository; - @Inject - @ConfigProperty(name = "wave.webhook.secret") - Optional waveWebhookSecret; + @Inject KeycloakService keycloakService; /** - * Crée une session de paiement Wave Checkout + * Crée un nouveau compte Wave + * + * @param compteWaveDTO DTO du compte à créer + * @return DTO du compte créé + */ + @Transactional + public CompteWaveDTO creerCompteWave(CompteWaveDTO compteWaveDTO) { + LOG.infof("Création d'un nouveau compte Wave: %s", compteWaveDTO.getNumeroTelephone()); + + // Vérifier l'unicité du numéro de téléphone + if (compteWaveRepository.findByNumeroTelephone(compteWaveDTO.getNumeroTelephone()).isPresent()) { + throw new IllegalArgumentException( + "Un compte Wave existe déjà pour ce numéro: " + compteWaveDTO.getNumeroTelephone()); + } + + CompteWave compteWave = convertToEntity(compteWaveDTO); + compteWave.setCreePar(keycloakService.getCurrentUserEmail()); + + compteWaveRepository.persist(compteWave); + LOG.infof("Compte Wave créé avec succès: ID=%s, Téléphone=%s", compteWave.getId(), compteWave.getNumeroTelephone()); + + return convertToDTO(compteWave); + } + + /** + * Met à jour un compte Wave + * + * @param id ID du compte + * @param compteWaveDTO DTO avec les modifications + * @return DTO du compte mis à jour + */ + @Transactional + public CompteWaveDTO mettreAJourCompteWave(UUID id, CompteWaveDTO compteWaveDTO) { + LOG.infof("Mise à jour du compte Wave ID: %s", id); + + CompteWave compteWave = + compteWaveRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Compte Wave non trouvé avec l'ID: " + id)); + + updateFromDTO(compteWave, compteWaveDTO); + compteWave.setModifiePar(keycloakService.getCurrentUserEmail()); + + compteWaveRepository.persist(compteWave); + LOG.infof("Compte Wave mis à jour avec succès: ID=%s", id); + + return convertToDTO(compteWave); + } + + /** + * Vérifie un compte Wave + * + * @param id ID du compte + * @return DTO du compte vérifié + */ + @Transactional + public CompteWaveDTO verifierCompteWave(UUID id) { + LOG.infof("Vérification du compte Wave ID: %s", id); + + CompteWave compteWave = + compteWaveRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Compte Wave non trouvé avec l'ID: " + id)); + + compteWave.setStatutCompte(StatutCompteWave.VERIFIE); + compteWave.setDateDerniereVerification(LocalDateTime.now()); + compteWave.setModifiePar(keycloakService.getCurrentUserEmail()); + + compteWaveRepository.persist(compteWave); + LOG.infof("Compte Wave vérifié avec succès: ID=%s", id); + + return convertToDTO(compteWave); + } + + /** + * Trouve un compte Wave par son ID + * + * @param id ID du compte + * @return DTO du compte + */ + public CompteWaveDTO trouverCompteWaveParId(UUID id) { + return compteWaveRepository + .findByIdOptional(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Compte Wave non trouvé avec l'ID: " + id)); + } + + /** + * Trouve un compte Wave par numéro de téléphone + * + * @param numeroTelephone Numéro de téléphone + * @return DTO du compte ou null + */ + public CompteWaveDTO trouverCompteWaveParTelephone(String numeroTelephone) { + return compteWaveRepository + .findByNumeroTelephone(numeroTelephone) + .map(this::convertToDTO) + .orElse(null); + } + + /** + * Liste tous les comptes Wave d'une organisation * - * @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 + * @return Liste des comptes Wave + */ + public List listerComptesWaveParOrganisation(UUID organisationId) { + return compteWaveRepository.findByOrganisationId(organisationId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Crée une nouvelle transaction Wave + * + * @param transactionWaveDTO DTO de la transaction à créer + * @return DTO de la transaction créée */ @Transactional - public WaveCheckoutSessionDTO creerSessionPaiement( - BigDecimal montant, - String devise, - String successUrl, - String errorUrl, - String referenceUnionFlow, - String description, - UUID organisationId, - UUID membreId) { + public TransactionWaveDTO creerTransactionWave(TransactionWaveDTO transactionWaveDTO) { + LOG.infof("Création d'une nouvelle transaction Wave: %s", transactionWaveDTO.getWaveTransactionId()); + + TransactionWave transactionWave = convertToEntity(transactionWaveDTO); + transactionWave.setCreePar(keycloakService.getCurrentUserEmail()); + + transactionWaveRepository.persist(transactionWave); LOG.infof( - "Création d'une session de paiement Wave: montant=%s, devise=%s, ref=%s", - montant, devise, referenceUnionFlow); + "Transaction Wave créée avec succès: ID=%s, WaveTransactionId=%s", + transactionWave.getId(), transactionWave.getWaveTransactionId()); - 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); - } + return convertToDTO(transactionWave); } /** - * Vérifie le statut d'une session de paiement + * Met à jour le statut d'une transaction Wave * - * @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é + * @param waveTransactionId Identifiant Wave de la transaction + * @param nouveauStatut Nouveau statut + * @return DTO de la transaction mise à jour */ @Transactional - public WaveWebhookDTO traiterWebhook(String payload, String signature, Map headers) { - LOG.info("Traitement d'un webhook Wave"); + public TransactionWaveDTO mettreAJourStatutTransaction( + String waveTransactionId, StatutTransactionWave nouveauStatut) { + LOG.infof("Mise à jour du statut de la transaction Wave: %s -> %s", waveTransactionId, nouveauStatut); - try { - // Vérifier la signature - if (!verifierSignatureWebhook(payload, signature)) { - LOG.warn("Signature webhook invalide"); - throw new SecurityException("Signature webhook invalide"); - } + TransactionWave transactionWave = + transactionWaveRepository + .findByWaveTransactionId(waveTransactionId) + .orElseThrow( + () -> + new NotFoundException( + "Transaction Wave non trouvée avec l'ID: " + waveTransactionId)); - // 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 + transactionWave.setStatutTransaction(nouveauStatut); + transactionWave.setDateDerniereTentative(LocalDateTime.now()); + transactionWave.setModifiePar(keycloakService.getCurrentUserEmail()); - 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()); + transactionWaveRepository.persist(transactionWave); + LOG.infof("Statut de la transaction Wave mis à jour: %s", waveTransactionId); - // 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); - } + return convertToDTO(transactionWave); } /** - * Consulte le solde du wallet Wave + * Trouve une transaction Wave par son identifiant Wave * - * @return Solde du wallet + * @param waveTransactionId Identifiant Wave + * @return DTO de la transaction */ - public WaveBalanceDTO consulterSolde() { - LOG.info("Consultation du solde Wave"); + public TransactionWaveDTO trouverTransactionWaveParId(String waveTransactionId) { + return transactionWaveRepository + .findByWaveTransactionId(waveTransactionId) + .map(this::convertToDTO) + .orElseThrow( + () -> new NotFoundException("Transaction Wave non trouvée avec l'ID: " + waveTransactionId)); + } - 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()); + // ======================================== + // MÉTHODES PRIVÉES + // ======================================== - return balance; + /** Convertit une entité CompteWave en DTO */ + private CompteWaveDTO convertToDTO(CompteWave compteWave) { + if (compteWave == null) { + return null; + } - } 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); + CompteWaveDTO dto = new CompteWaveDTO(); + dto.setId(compteWave.getId()); + dto.setNumeroTelephone(compteWave.getNumeroTelephone()); + dto.setStatutCompte(compteWave.getStatutCompte()); + dto.setWaveAccountId(compteWave.getWaveAccountId()); + dto.setEnvironnement(compteWave.getEnvironnement()); + dto.setDateDerniereVerification(compteWave.getDateDerniereVerification()); + dto.setCommentaire(compteWave.getCommentaire()); + + if (compteWave.getOrganisation() != null) { + dto.setOrganisationId(compteWave.getOrganisation().getId()); + } + if (compteWave.getMembre() != null) { + dto.setMembreId(compteWave.getMembre().getId()); + } + + dto.setDateCreation(compteWave.getDateCreation()); + dto.setDateModification(compteWave.getDateModification()); + dto.setActif(compteWave.getActif()); + + return dto; + } + + /** Convertit un DTO en entité CompteWave */ + private CompteWave convertToEntity(CompteWaveDTO dto) { + if (dto == null) { + return null; + } + + CompteWave compteWave = new CompteWave(); + compteWave.setNumeroTelephone(dto.getNumeroTelephone()); + compteWave.setStatutCompte(dto.getStatutCompte() != null ? dto.getStatutCompte() : StatutCompteWave.NON_VERIFIE); + compteWave.setWaveAccountId(dto.getWaveAccountId()); + compteWave.setEnvironnement(dto.getEnvironnement() != null ? dto.getEnvironnement() : "SANDBOX"); + compteWave.setDateDerniereVerification(dto.getDateDerniereVerification()); + compteWave.setCommentaire(dto.getCommentaire()); + + // Relations + if (dto.getOrganisationId() != null) { + Organisation org = + organisationRepository + .findByIdOptional(dto.getOrganisationId()) + .orElseThrow( + () -> + new NotFoundException( + "Organisation non trouvée avec l'ID: " + dto.getOrganisationId())); + compteWave.setOrganisation(org); + } + + if (dto.getMembreId() != null) { + Membre membre = + membreRepository + .findByIdOptional(dto.getMembreId()) + .orElseThrow( + () -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId())); + compteWave.setMembre(membre); + } + + return compteWave; + } + + /** Met à jour une entité CompteWave à partir d'un DTO */ + private void updateFromDTO(CompteWave compteWave, CompteWaveDTO dto) { + if (dto.getStatutCompte() != null) { + compteWave.setStatutCompte(dto.getStatutCompte()); + } + if (dto.getWaveAccountId() != null) { + compteWave.setWaveAccountId(dto.getWaveAccountId()); + } + if (dto.getEnvironnement() != null) { + compteWave.setEnvironnement(dto.getEnvironnement()); + } + if (dto.getDateDerniereVerification() != null) { + compteWave.setDateDerniereVerification(dto.getDateDerniereVerification()); + } + if (dto.getCommentaire() != null) { + compteWave.setCommentaire(dto.getCommentaire()); } } - /** - * 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(); + /** Convertit une entité TransactionWave en DTO */ + private TransactionWaveDTO convertToDTO(TransactionWave transactionWave) { + if (transactionWave == null) { + return null; + } + + TransactionWaveDTO dto = new TransactionWaveDTO(); + dto.setId(transactionWave.getId()); + dto.setWaveTransactionId(transactionWave.getWaveTransactionId()); + dto.setWaveRequestId(transactionWave.getWaveRequestId()); + dto.setWaveReference(transactionWave.getWaveReference()); + dto.setTypeTransaction(transactionWave.getTypeTransaction()); + dto.setStatutTransaction(transactionWave.getStatutTransaction()); + dto.setMontant(transactionWave.getMontant()); + dto.setFrais(transactionWave.getFrais()); + dto.setMontantNet(transactionWave.getMontantNet()); + dto.setCodeDevise(transactionWave.getCodeDevise()); + dto.setTelephonePayeur(transactionWave.getTelephonePayeur()); + dto.setTelephoneBeneficiaire(transactionWave.getTelephoneBeneficiaire()); + dto.setMetadonnees(transactionWave.getMetadonnees()); + dto.setNombreTentatives(transactionWave.getNombreTentatives()); + dto.setDateDerniereTentative(transactionWave.getDateDerniereTentative()); + dto.setMessageErreur(transactionWave.getMessageErreur()); + + if (transactionWave.getCompteWave() != null) { + dto.setCompteWaveId(transactionWave.getCompteWave().getId()); + } + + dto.setDateCreation(transactionWave.getDateCreation()); + dto.setDateModification(transactionWave.getDateModification()); + dto.setActif(transactionWave.getActif()); + + return dto; } - /** - * Teste la connexion à l'API Wave - * - * @return Résultat du test - */ - public Map testerConnexion() { - LOG.info("Test de connexion à l'API Wave"); - - Map resultat = new HashMap<>(); - resultat.put("configure", estConfigure()); - resultat.put("environment", waveEnvironment); - resultat.put("baseUrl", waveApiBaseUrl); - resultat.put("timestamp", LocalDateTime.now().toString()); - - if (!estConfigure()) { - resultat.put("statut", "ERREUR"); - resultat.put("message", "Wave n'est pas configuré (clés API manquantes)"); - return resultat; + /** Convertit un DTO en entité TransactionWave */ + private TransactionWave convertToEntity(TransactionWaveDTO dto) { + if (dto == null) { + return null; } - 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; + TransactionWave transactionWave = new TransactionWave(); + transactionWave.setWaveTransactionId(dto.getWaveTransactionId()); + transactionWave.setWaveRequestId(dto.getWaveRequestId()); + transactionWave.setWaveReference(dto.getWaveReference()); + transactionWave.setTypeTransaction(dto.getTypeTransaction()); + transactionWave.setStatutTransaction( + dto.getStatutTransaction() != null + ? dto.getStatutTransaction() + : StatutTransactionWave.INITIALISE); + transactionWave.setMontant(dto.getMontant()); + transactionWave.setFrais(dto.getFrais()); + transactionWave.setMontantNet(dto.getMontantNet()); + transactionWave.setCodeDevise(dto.getCodeDevise() != null ? dto.getCodeDevise() : "XOF"); + transactionWave.setTelephonePayeur(dto.getTelephonePayeur()); + transactionWave.setTelephoneBeneficiaire(dto.getTelephoneBeneficiaire()); + transactionWave.setMetadonnees(dto.getMetadonnees()); + transactionWave.setNombreTentatives(dto.getNombreTentatives() != null ? dto.getNombreTentatives() : 0); + transactionWave.setDateDerniereTentative(dto.getDateDerniereTentative()); + transactionWave.setMessageErreur(dto.getMessageErreur()); - } 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; + // Relation CompteWave + if (dto.getCompteWaveId() != null) { + CompteWave compteWave = + compteWaveRepository + .findByIdOptional(dto.getCompteWaveId()) + .orElseThrow( + () -> + new NotFoundException( + "Compte Wave non trouvé avec l'ID: " + dto.getCompteWaveId())); + transactionWave.setCompteWave(compteWave); } - if (!waveWebhookSecret.isPresent() || waveWebhookSecret.get().isEmpty()) { - LOG.warn("Secret webhook non configuré, impossible de vérifier la signature"); - return false; - } - - // TODO: Implémenter la vérification réelle de la signature HMAC SHA256 - // La signature Wave est généralement au format: sha256= - return true; // Pour l'instant, on accepte toutes les signatures + return transactionWave; } } -