feat: DTOs, Services et Resources REST pour Paiements et Wave

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
This commit is contained in:
dahoud
2025-11-30 02:29:48 +00:00
parent e53440da24
commit 5a7ead81f7
8 changed files with 1224 additions and 496 deletions

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import jakarta.persistence.*;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;

View File

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

View File

@@ -1,213 +1,264 @@
package dev.lions.unionflow.server.resource; package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.paiement.WaveBalanceDTO; import dev.lions.unionflow.server.api.dto.wave.CompteWaveDTO;
import dev.lions.unionflow.server.api.dto.paiement.WaveCheckoutSessionDTO; import dev.lions.unionflow.server.api.dto.wave.TransactionWaveDTO;
import dev.lions.unionflow.server.api.dto.paiement.WaveWebhookDTO; import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave;
import dev.lions.unionflow.server.service.WaveService; import dev.lions.unionflow.server.service.WaveService;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin; import jakarta.ws.rs.*;
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.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.math.BigDecimal; import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; 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; 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 * @author UnionFlow Team
* @version 1.0 * @version 3.0
* @since 2025-01-17 * @since 2025-01-29
*/ */
@Path("/api/wave") @Path("/api/wave")
@Tag(name = "Wave Money", description = "API d'intégration Wave Money pour les paiements")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@PermitAll
public class WaveResource { public class WaveResource {
private static final Logger LOG = Logger.getLogger(WaveResource.class); private static final Logger LOG = Logger.getLogger(WaveResource.class);
@Inject WaveService waveService; @Inject WaveService waveService;
// ========================================
// COMPTES WAVE
// ========================================
/**
* Crée un nouveau compte Wave
*
* @param compteWaveDTO DTO du compte à créer
* @return Compte créé
*/
@POST @POST
@Path("/checkout/sessions") @Path("/comptes")
@Operation( public Response creerCompteWave(@Valid CompteWaveDTO compteWaveDTO) {
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 { try {
WaveCheckoutSessionDTO session = CompteWaveDTO result = waveService.creerCompteWave(compteWaveDTO);
waveService.creerSessionPaiement( return Response.status(Response.Status.CREATED).entity(result).build();
montant, } catch (IllegalArgumentException e) {
devise, return Response.status(Response.Status.BAD_REQUEST)
successUrl, .entity(new ErrorResponse(e.getMessage()))
errorUrl, .build();
referenceUnionFlow,
description,
organisationId,
membreId);
return Response.ok(session).build();
} catch (Exception e) { } catch (Exception e) {
LOG.errorf(e, "Erreur lors de la création de la session: %s", e.getMessage()); LOG.errorf(e, "Erreur lors de la création du compte Wave");
Map<String, String> erreur = new HashMap<>(); return Response.status(Response.Status.BAD_REQUEST)
erreur.put("erreur", "Erreur lors de la création de la session"); .entity(new ErrorResponse("Erreur lors de la création du compte Wave: " + e.getMessage()))
erreur.put("message", e.getMessage()); .build();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build();
} }
} }
@GET /**
@Path("/checkout/sessions/{sessionId}") * Met à jour un compte Wave
@Operation( *
summary = "Vérifier le statut d'une session", * @param id ID du compte
description = "Récupère le statut d'une session de paiement Wave") * @param compteWaveDTO DTO avec les modifications
@APIResponse( * @return Compte mis à jour
responseCode = "200", */
description = "Statut récupéré avec succès", @PUT
content = @Path("/comptes/{id}")
@Content( public Response mettreAJourCompteWave(@PathParam("id") UUID id, @Valid CompteWaveDTO compteWaveDTO) {
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 { try {
WaveCheckoutSessionDTO session = waveService.verifierStatutSession(sessionId); CompteWaveDTO result = waveService.mettreAJourCompteWave(id, compteWaveDTO);
return Response.ok(session).build(); 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) { } catch (Exception e) {
LOG.errorf(e, "Erreur lors de la vérification du statut: %s", e.getMessage()); LOG.errorf(e, "Erreur lors de la mise à jour du compte Wave");
Map<String, String> erreur = new HashMap<>(); return Response.status(Response.Status.BAD_REQUEST)
erreur.put("erreur", "Erreur lors de la vérification du statut"); .entity(new ErrorResponse("Erreur lors de la mise à jour du compte Wave: " + e.getMessage()))
erreur.put("message", e.getMessage()); .build();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build();
} }
} }
/**
* Vérifie un compte Wave
*
* @param id ID du compte
* @return Compte vérifié
*/
@POST @POST
@Path("/webhooks") @Path("/comptes/{id}/verifier")
@Operation( public Response verifierCompteWave(@PathParam("id") UUID id) {
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 { try {
// Récupérer les headers CompteWaveDTO result = waveService.verifierCompteWave(id);
Map<String, String> headers = new HashMap<>(); return Response.ok(result).build();
if (signature != null) { } catch (jakarta.ws.rs.NotFoundException e) {
headers.put("X-Wave-Signature", signature); 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();
} catch (Exception e) { } catch (Exception e) {
LOG.errorf(e, "Erreur lors du traitement du webhook: %s", e.getMessage()); LOG.errorf(e, "Erreur lors de la vérification du compte Wave");
Map<String, String> erreur = new HashMap<>(); return Response.status(Response.Status.BAD_REQUEST)
erreur.put("erreur", "Erreur lors du traitement du webhook"); .entity(new ErrorResponse("Erreur lors de la vérification du compte Wave: " + e.getMessage()))
erreur.put("message", e.getMessage()); .build();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build();
} }
} }
/**
* Trouve un compte Wave par son ID
*
* @param id ID du compte
* @return Compte Wave
*/
@GET @GET
@Path("/balance") @Path("/comptes/{id}")
@Operation( public Response trouverCompteWaveParId(@PathParam("id") UUID id) {
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 { try {
WaveBalanceDTO balance = waveService.consulterSolde(); CompteWaveDTO result = waveService.trouverCompteWaveParId(id);
return Response.ok(balance).build(); 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) { } catch (Exception e) {
LOG.errorf(e, "Erreur lors de la consultation du solde: %s", e.getMessage()); LOG.errorf(e, "Erreur lors de la recherche du compte Wave");
Map<String, String> erreur = new HashMap<>(); return Response.status(Response.Status.BAD_REQUEST)
erreur.put("erreur", "Erreur lors de la consultation du solde"); .entity(new ErrorResponse("Erreur lors de la recherche du compte Wave: " + e.getMessage()))
erreur.put("message", e.getMessage()); .build();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(erreur).build();
} }
} }
/**
* 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 @GET
@Path("/test") @Path("/comptes/telephone/{numeroTelephone}")
@Operation( public Response trouverCompteWaveParTelephone(@PathParam("numeroTelephone") String numeroTelephone) {
summary = "Tester la connexion Wave", try {
description = "Teste la connexion et la configuration de l'API Wave") CompteWaveDTO result = waveService.trouverCompteWaveParTelephone(numeroTelephone);
@APIResponse(responseCode = "200", description = "Test effectué") if (result == null) {
public Response testerConnexion() { return Response.status(Response.Status.NOT_FOUND)
Map<String, Object> resultat = waveService.testerConnexion(); .entity(new ErrorResponse("Compte Wave non trouvé"))
return Response.ok(resultat).build(); .build();
}
return Response.ok(result).build();
} catch (Exception e) {
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("/comptes/organisation/{organisationId}")
public Response listerComptesWaveParOrganisation(@PathParam("organisationId") UUID organisationId) {
try {
List<CompteWaveDTO> result = waveService.listerComptesWaveParOrganisation(organisationId);
return Response.ok(result).build();
} catch (Exception e) {
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("/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;
}
}
}

View File

@@ -1,174 +1,309 @@
package dev.lions.unionflow.server.service; 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.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.Valid; import jakarta.ws.rs.NotFoundException;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
/** /**
* Service métier pour la gestion des paiements Mobile Money Intègre Wave Money, Orange Money, et * Service métier pour la gestion des paiements
* Moov Money
* *
* @author UnionFlow Team * @author UnionFlow Team
* @version 1.0 * @version 3.0
* @since 2025-01-15 * @since 2025-01-29
*/ */
@ApplicationScoped @ApplicationScoped
public class PaiementService { public class PaiementService {
private static final Logger LOG = Logger.getLogger(PaiementService.class); 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 * @param paiementDTO DTO du paiement à créer
* @return informations du paiement initié * @return DTO du paiement créé
*/ */
@Transactional @Transactional
public Map<String, Object> initiatePayment(@Valid Map<String, Object> paymentData) { public PaiementDTO creerPaiement(PaiementDTO paiementDTO) {
LOG.infof("Initiation d'un paiement"); LOG.infof("Création d'un nouveau paiement: %s", paiementDTO.getNumeroReference());
try { Paiement paiement = convertToEntity(paiementDTO);
String operateur = (String) paymentData.get("operateur"); paiement.setCreePar(keycloakService.getCurrentUserEmail());
BigDecimal montant = new BigDecimal(paymentData.get("montant").toString());
String numeroTelephone = (String) paymentData.get("numeroTelephone");
String cotisationId = (String) paymentData.get("cotisationId");
// Générer un ID unique pour le paiement paiementRepository.persist(paiement);
String paymentId = UUID.randomUUID().toString(); LOG.infof("Paiement créé avec succès: ID=%s, Référence=%s", paiement.getId(), paiement.getNumeroReference());
String numeroReference = "PAY-" + System.currentTimeMillis();
Map<String, Object> response = new HashMap<>(); return convertToDTO(paiement);
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<String, Object> 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());
}
} }
/** /**
* Récupère le statut d'un paiement * Met à jour un paiement existant
* *
* @param paymentId ID du paiement * @param id ID du paiement
* @return statut du paiement * @param paiementDTO DTO avec les modifications
* @return DTO du paiement mis à jour
*/ */
public Map<String, Object> getPaymentStatus(@NotNull String paymentId) { @Transactional
LOG.infof("Récupération du statut du paiement: %s", paymentId); public PaiementDTO mettreAJourPaiement(UUID id, PaiementDTO paiementDTO) {
LOG.infof("Mise à jour du paiement ID: %s", id);
// Simulation du statut Paiement paiement =
Map<String, Object> status = new HashMap<>(); paiementRepository
status.put("id", paymentId); .findByIdOptional(id)
status.put("statut", "COMPLETED"); // Simulation d'un paiement réussi .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id));
status.put("dateModification", LocalDateTime.now().toString());
status.put("message", "Paiement traité avec succès");
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 * Annule un paiement
* *
* @param paymentId ID du paiement * @param id ID du paiement
* @param cotisationId ID de la cotisation * @return DTO du paiement annulé
* @return résultat de l'annulation
*/ */
@Transactional @Transactional
public Map<String, Object> cancelPayment( public PaiementDTO annulerPaiement(UUID id) {
@NotNull String paymentId, @NotNull String cotisationId) { LOG.infof("Annulation du paiement ID: %s", id);
LOG.infof("Annulation du paiement: %s pour cotisation: %s", paymentId, cotisationId);
Map<String, Object> result = new HashMap<>(); Paiement paiement =
result.put("id", paymentId); paiementRepository
result.put("cotisationId", cotisationId); .findByIdOptional(id)
result.put("statut", "CANCELLED"); .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id));
result.put("dateAnnulation", LocalDateTime.now().toString());
result.put("message", "Paiement annulé avec succès");
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 * @param id ID du paiement
* @return liste des paiements * @return DTO du paiement
*/ */
public List<Map<String, Object>> getPaymentHistory(Map<String, Object> filters) { public PaiementDTO trouverParId(UUID id) {
LOG.info("Récupération de l'historique des paiements"); return paiementRepository
.findByIdOptional(id)
// Simulation d'un historique vide pour l'instant .map(this::convertToDTO)
return List.of(); .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) * @param numeroReference Numéro de référence
* @return statut du service * @return DTO du paiement
*/ */
public Map<String, Object> checkServiceStatus(@NotNull String serviceType) { public PaiementDTO trouverParNumeroReference(String numeroReference) {
LOG.infof("Vérification du statut du service: %s", serviceType); return paiementRepository
.findByNumeroReference(numeroReference)
Map<String, Object> status = new HashMap<>(); .map(this::convertToDTO)
status.put("service", serviceType); .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec la référence: " + numeroReference));
status.put("statut", "OPERATIONAL");
status.put("disponible", true);
status.put("derniereMiseAJour", LocalDateTime.now().toString());
return status;
} }
/** /**
* Récupère les statistiques de paiement * Liste tous les paiements d'un membre
* *
* @param filters filtres pour les statistiques * @param membreId ID du membre
* @return statistiques des paiements * @return Liste des paiements
*/ */
public Map<String, Object> getPaymentStatistics(Map<String, Object> filters) { public List<PaiementDTO> listerParMembre(UUID membreId) {
LOG.info("Récupération des statistiques de paiement"); return paiementRepository.findByMembreId(membreId).stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
Map<String, Object> stats = new HashMap<>(); /**
stats.put("totalPaiements", 0); * Calcule le montant total des paiements validés dans une période
stats.put("montantTotal", BigDecimal.ZERO); *
stats.put("paiementsReussis", 0); * @param dateDebut Date de début
stats.put("paiementsEchoues", 0); * @param dateFin Date de fin
stats.put("paiementsEnAttente", 0); * @return Montant total
stats.put( */
"operateurs", public BigDecimal calculerMontantTotalValides(LocalDateTime dateDebut, LocalDateTime dateFin) {
Map.of( return paiementRepository.calculerMontantTotalValides(dateDebut, dateFin);
"WAVE", 0, }
"ORANGE_MONEY", 0,
"MOOV_MONEY", 0));
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());
}
} }
} }

View File

@@ -1,281 +1,393 @@
package dev.lions.unionflow.server.service; package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.paiement.WaveBalanceDTO; import dev.lions.unionflow.server.api.dto.wave.CompteWaveDTO;
import dev.lions.unionflow.server.api.dto.paiement.WaveCheckoutSessionDTO; import dev.lions.unionflow.server.api.dto.wave.TransactionWaveDTO;
import dev.lions.unionflow.server.api.dto.paiement.WaveWebhookDTO; import dev.lions.unionflow.server.api.enums.wave.StatutCompteWave;
import dev.lions.unionflow.server.api.enums.paiement.StatutSession; import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave;
import dev.lions.unionflow.server.api.enums.paiement.TypeEvenement; 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.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.eclipse.microprofile.config.inject.ConfigProperty; import java.util.stream.Collectors;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
/** /**
* Service pour l'intégration Wave Money * Service métier pour l'intégration Wave Mobile Money
* Gère les sessions de paiement, les webhooks et la consultation du solde
* *
* @author UnionFlow Team * @author UnionFlow Team
* @version 1.0 * @version 3.0
* @since 2025-01-17 * @since 2025-01-29
*/ */
@ApplicationScoped @ApplicationScoped
public class WaveService { public class WaveService {
private static final Logger LOG = Logger.getLogger(WaveService.class); private static final Logger LOG = Logger.getLogger(WaveService.class);
@Inject @Inject CompteWaveRepository compteWaveRepository;
@ConfigProperty(name = "wave.api.key")
Optional<String> waveApiKey;
@Inject @Inject TransactionWaveRepository transactionWaveRepository;
@ConfigProperty(name = "wave.api.secret")
Optional<String> waveApiSecret;
@Inject @Inject OrganisationRepository organisationRepository;
@ConfigProperty(name = "wave.api.base.url", defaultValue = "https://api.wave.com/v1")
String waveApiBaseUrl;
@Inject @Inject MembreRepository membreRepository;
@ConfigProperty(name = "wave.environment", defaultValue = "sandbox")
String waveEnvironment;
@Inject @Inject KeycloakService keycloakService;
@ConfigProperty(name = "wave.webhook.secret")
Optional<String> waveWebhookSecret;
/** /**
* 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 organisationId ID de l'organisation
* @param membreId ID du membre * @return Liste des comptes Wave
* @return Session de paiement créée */
public List<CompteWaveDTO> 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 @Transactional
public WaveCheckoutSessionDTO creerSessionPaiement( public TransactionWaveDTO creerTransactionWave(TransactionWaveDTO transactionWaveDTO) {
BigDecimal montant, LOG.infof("Création d'une nouvelle transaction Wave: %s", transactionWaveDTO.getWaveTransactionId());
String devise,
String successUrl, TransactionWave transactionWave = convertToEntity(transactionWaveDTO);
String errorUrl, transactionWave.setCreePar(keycloakService.getCurrentUserEmail());
String referenceUnionFlow,
String description, transactionWaveRepository.persist(transactionWave);
UUID organisationId,
UUID membreId) {
LOG.infof( LOG.infof(
"Création d'une session de paiement Wave: montant=%s, devise=%s, ref=%s", "Transaction Wave créée avec succès: ID=%s, WaveTransactionId=%s",
montant, devise, referenceUnionFlow); transactionWave.getId(), transactionWave.getWaveTransactionId());
try { return convertToDTO(transactionWave);
// 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 * Met à jour le statut d'une transaction Wave
* *
* @param waveSessionId ID de la session Wave * @param waveTransactionId Identifiant Wave de la transaction
* @return Statut de la session * @param nouveauStatut Nouveau statut
*/ * @return DTO de la transaction mise à jour
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 @Transactional
public WaveWebhookDTO traiterWebhook(String payload, String signature, Map<String, String> headers) { public TransactionWaveDTO mettreAJourStatutTransaction(
LOG.info("Traitement d'un webhook Wave"); String waveTransactionId, StatutTransactionWave nouveauStatut) {
LOG.infof("Mise à jour du statut de la transaction Wave: %s -> %s", waveTransactionId, nouveauStatut);
try { TransactionWave transactionWave =
// Vérifier la signature transactionWaveRepository
if (!verifierSignatureWebhook(payload, signature)) { .findByWaveTransactionId(waveTransactionId)
LOG.warn("Signature webhook invalide"); .orElseThrow(
throw new SecurityException("Signature webhook invalide"); () ->
} new NotFoundException(
"Transaction Wave non trouvée avec l'ID: " + waveTransactionId));
// Parser le payload transactionWave.setStatutTransaction(nouveauStatut);
// TODO: Parser réellement le JSON du webhook transactionWave.setDateDerniereTentative(LocalDateTime.now());
String webhookId = "webhook_" + UUID.randomUUID().toString(); transactionWave.setModifiePar(keycloakService.getCurrentUserEmail());
TypeEvenement typeEvenement = TypeEvenement.CHECKOUT_COMPLETE; // À déterminer depuis le payload
WaveWebhookDTO webhook = new WaveWebhookDTO(); transactionWaveRepository.persist(transactionWave);
webhook.setId(UUID.randomUUID()); LOG.infof("Statut de la transaction Wave mis à jour: %s", waveTransactionId);
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 return convertToDTO(transactionWave);
// 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 * Trouve une transaction Wave par son identifiant Wave
* *
* @return Solde du wallet * @param waveTransactionId Identifiant Wave
* @return DTO de la transaction
*/ */
public WaveBalanceDTO consulterSolde() { public TransactionWaveDTO trouverTransactionWaveParId(String waveTransactionId) {
LOG.info("Consultation du solde Wave"); 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 // MÉTHODES PRIVÉES
// 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; /** Convertit une entité CompteWave en DTO */
private CompteWaveDTO convertToDTO(CompteWave compteWave) {
if (compteWave == null) {
return null;
}
} catch (Exception e) { CompteWaveDTO dto = new CompteWaveDTO();
LOG.errorf(e, "Erreur lors de la consultation du solde: %s", e.getMessage()); dto.setId(compteWave.getId());
throw new RuntimeException("Erreur lors de la consultation du solde", e); 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());
} }
} }
/** /** Convertit une entité TransactionWave en DTO */
* Vérifie si Wave est configuré et actif private TransactionWaveDTO convertToDTO(TransactionWave transactionWave) {
* if (transactionWave == null) {
* @return true si Wave est configuré return null;
*/
public boolean estConfigure() {
return waveApiKey.isPresent() && !waveApiKey.get().isEmpty()
&& waveApiSecret.isPresent() && !waveApiSecret.get().isEmpty();
} }
/** TransactionWaveDTO dto = new TransactionWaveDTO();
* Teste la connexion à l'API Wave dto.setId(transactionWave.getId());
* dto.setWaveTransactionId(transactionWave.getWaveTransactionId());
* @return Résultat du test dto.setWaveRequestId(transactionWave.getWaveRequestId());
*/ dto.setWaveReference(transactionWave.getWaveReference());
public Map<String, Object> testerConnexion() { dto.setTypeTransaction(transactionWave.getTypeTransaction());
LOG.info("Test de connexion à l'API Wave"); 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());
Map<String, Object> resultat = new HashMap<>(); if (transactionWave.getCompteWave() != null) {
resultat.put("configure", estConfigure()); dto.setCompteWaveId(transactionWave.getCompteWave().getId());
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 { dto.setDateCreation(transactionWave.getDateCreation());
// TODO: Faire un appel réel à l'API Wave pour tester dto.setDateModification(transactionWave.getDateModification());
resultat.put("statut", "OK"); dto.setActif(transactionWave.getActif());
resultat.put("message", "Connexion réussie (simulation)");
return resultat;
} catch (Exception e) { return dto;
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 /** Convertit un DTO en entité TransactionWave */
private TransactionWave convertToEntity(TransactionWaveDTO dto) {
private String buildWaveCheckoutUrl(String sessionId) { if (dto == null) {
if ("sandbox".equals(waveEnvironment)) { return null;
return "https://checkout-sandbox.wave.com/checkout/" + sessionId;
} else {
return "https://checkout.wave.com/checkout/" + sessionId;
}
} }
private boolean verifierSignatureWebhook(String payload, String signature) { TransactionWave transactionWave = new TransactionWave();
if (signature == null || signature.isEmpty()) { transactionWave.setWaveTransactionId(dto.getWaveTransactionId());
return false; 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());
// 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()) { return transactionWave;
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
} }
} }