diff --git a/src/main/java/dev/lions/unionflow/server/entity/Paiement.java b/src/main/java/dev/lions/unionflow/server/entity/Paiement.java index dce1a1f..7a3ad15 100644 --- a/src/main/java/dev/lions/unionflow/server/entity/Paiement.java +++ b/src/main/java/dev/lions/unionflow/server/entity/Paiement.java @@ -5,7 +5,6 @@ import jakarta.persistence.*; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime; -import java.util.concurrent.atomic.AtomicLong; import java.util.ArrayList; import java.util.List; import lombok.AllArgsConstructor; @@ -15,8 +14,8 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; /** - * Entité Paiement centralisée pour tous les types de paiements - * Réutilisable pour cotisations, adhésions, événements, aides + * Entité Paiement centralisée pour tous les types de paiements. + * Réutilisable pour cotisations, adhésions, événements, aides. * * @author UnionFlow Team * @version 3.0 @@ -104,7 +103,7 @@ public class Paiement extends BaseEntity { @JoinColumn(name = "membre_id", nullable = false) private Membre membre; - /** Objets cibles de ce paiement (Cat.2 — polymorphique) */ + /** Objets cibles de ce paiement (polymorphique) */ @JsonIgnore @OneToMany(mappedBy = "paiement", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default @@ -115,18 +114,15 @@ public class Paiement extends BaseEntity { @JoinColumn(name = "transaction_wave_id") private TransactionWave transactionWave; - private static final AtomicLong REFERENCE_COUNTER = - new AtomicLong(System.currentTimeMillis() % 1000000000000L); - - /** Méthode métier pour générer un numéro de référence unique */ + /** Génère un numéro de référence unique */ public static String genererNumeroReference() { return "PAY-" + LocalDateTime.now().getYear() + "-" - + String.format("%012d", REFERENCE_COUNTER.incrementAndGet() % 1000000000000L); + + String.format("%012d", System.currentTimeMillis() % 1000000000000L); } - /** Méthode métier pour vérifier si le paiement est validé */ + /** Vérifie si le paiement est validé */ public boolean isValide() { return "VALIDE".equals(statutPaiement); } @@ -137,12 +133,10 @@ public class Paiement extends BaseEntity { && !"ANNULE".equals(statutPaiement); } - /** Callback JPA avant la persistance */ @PrePersist protected void onCreate() { super.onCreate(); - if (numeroReference == null - || numeroReference.isEmpty()) { + if (numeroReference == null || numeroReference.isEmpty()) { numeroReference = genererNumeroReference(); } if (statutPaiement == null) { diff --git a/src/main/java/dev/lions/unionflow/server/entity/PaiementObjet.java b/src/main/java/dev/lions/unionflow/server/entity/PaiementObjet.java index 4a37a0d..67cc8b6 100644 --- a/src/main/java/dev/lions/unionflow/server/entity/PaiementObjet.java +++ b/src/main/java/dev/lions/unionflow/server/entity/PaiementObjet.java @@ -1,19 +1,7 @@ package dev.lions.unionflow.server.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.Index; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.PrePersist; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; -import jakarta.validation.constraints.DecimalMin; -import jakarta.validation.constraints.Digits; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.UUID; @@ -24,23 +12,11 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; /** - * Table de liaison polymorphique entre un paiement - * et son objet cible. + * Table de liaison polymorphique entre un paiement et son objet cible. * - *

- * Remplace les 4 tables dupliquées - * {@code paiements_cotisations}, - * {@code paiements_adhesions}, - * {@code paiements_evenements} et - * {@code paiements_aides} par une table unique - * utilisant le pattern - * {@code (type_objet_cible, objet_cible_id)}. - * - *

- * Les types d'objet cible sont définis dans le - * domaine {@code OBJET_PAIEMENT} de la table - * {@code types_reference} (ex: COTISATION, - * ADHESION, EVENEMENT, AIDE). + *

Remplace les tables dupliquées {@code paiements_cotisations}, + * {@code paiements_adhesions}, etc. par une table unique utilisant + * le pattern {@code (type_objet_cible, objet_cible_id)}. * * @author UnionFlow Team * @version 3.0 @@ -48,16 +24,12 @@ import lombok.NoArgsConstructor; */ @Entity @Table(name = "paiements_objets", indexes = { - @Index(name = "idx_po_paiement", columnList = "paiement_id"), - @Index(name = "idx_po_objet", columnList = "type_objet_cible," - + " objet_cible_id"), - @Index(name = "idx_po_type", columnList = "type_objet_cible") + @Index(name = "idx_po_paiement", columnList = "paiement_id"), + @Index(name = "idx_po_objet", columnList = "type_objet_cible, objet_cible_id"), + @Index(name = "idx_po_type", columnList = "type_objet_cible") }, uniqueConstraints = { - @UniqueConstraint(name = "uk_paiement_objet", columnNames = { - "paiement_id", - "type_objet_cible", - "objet_cible_id" - }) + @UniqueConstraint(name = "uk_paiement_objet", + columnNames = {"paiement_id", "type_objet_cible", "objet_cible_id"}) }) @Data @NoArgsConstructor @@ -66,65 +38,47 @@ import lombok.NoArgsConstructor; @EqualsAndHashCode(callSuper = true) public class PaiementObjet extends BaseEntity { - /** Paiement parent. */ - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "paiement_id", nullable = false) - private Paiement paiement; + /** Paiement parent. */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "paiement_id", nullable = false) + private Paiement paiement; - /** - * Type de l'objet cible (code du domaine - * {@code OBJET_PAIEMENT} dans - * {@code types_reference}). - * - *

- * Valeurs attendues : {@code COTISATION}, - * {@code ADHESION}, {@code EVENEMENT}, - * {@code AIDE}. - */ - @NotBlank - @Size(max = 50) - @Column(name = "type_objet_cible", nullable = false, length = 50) - private String typeObjetCible; + /** + * Type de l'objet cible (ex: COTISATION, ADHESION, EVENEMENT, AIDE). + */ + @NotBlank + @Size(max = 50) + @Column(name = "type_objet_cible", nullable = false, length = 50) + private String typeObjetCible; - /** - * UUID de l'objet cible (cotisation, demande - * d'adhésion, inscription événement, ou demande - * d'aide). - */ - @NotNull - @Column(name = "objet_cible_id", nullable = false) - private UUID objetCibleId; + /** UUID de l'objet cible. */ + @NotNull + @Column(name = "objet_cible_id", nullable = false) + private UUID objetCibleId; - /** Montant appliqué à cet objet cible. */ - @NotNull - @DecimalMin(value = "0.0", message = "Le montant doit être positif") - @Digits(integer = 12, fraction = 2) - @Column(name = "montant_applique", nullable = false, precision = 14, scale = 2) - private BigDecimal montantApplique; + /** Montant appliqué à cet objet cible. */ + @NotNull + @DecimalMin(value = "0.0", message = "Le montant doit être positif") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_applique", nullable = false, precision = 14, scale = 2) + private BigDecimal montantApplique; - /** Date d'application du paiement. */ - @Column(name = "date_application") - private LocalDateTime dateApplication; + /** Date d'application du paiement. */ + @Column(name = "date_application") + private LocalDateTime dateApplication; - /** Commentaire sur l'application. */ - @Size(max = 500) - @Column(name = "commentaire", length = 500) - private String commentaire; + /** Commentaire sur l'application. */ + @Size(max = 500) + @Column(name = "commentaire", length = 500) + private String commentaire; - /** - * Callback JPA avant la persistance. - * - *

- * Initialise {@code dateApplication} si non - * renseignée. - */ - @Override - @PrePersist - protected void onCreate() { - super.onCreate(); - if (dateApplication == null) { - dateApplication = LocalDateTime.now(); - } + @Override + @PrePersist + protected void onCreate() { + super.onCreate(); + if (dateApplication == null) { + dateApplication = LocalDateTime.now(); } + } } diff --git a/src/main/java/dev/lions/unionflow/server/repository/PaiementRepository.java b/src/main/java/dev/lions/unionflow/server/repository/PaiementRepository.java index 99490c0..7849e08 100644 --- a/src/main/java/dev/lions/unionflow/server/repository/PaiementRepository.java +++ b/src/main/java/dev/lions/unionflow/server/repository/PaiementRepository.java @@ -1,10 +1,7 @@ package dev.lions.unionflow.server.repository; -import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement; -import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement; import dev.lions.unionflow.server.entity.Paiement; import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; -import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; import jakarta.enterprise.context.ApplicationScoped; import java.math.BigDecimal; @@ -14,7 +11,7 @@ import java.util.Optional; import java.util.UUID; /** - * Repository pour l'entité Paiement + * Repository pour l'entité Paiement. * * @author UnionFlow Team * @version 3.0 @@ -23,90 +20,57 @@ import java.util.UUID; @ApplicationScoped public class PaiementRepository implements PanacheRepositoryBase { - /** - * Trouve un paiement par son UUID - * - * @param id UUID du paiement - * @return Paiement ou Optional.empty() - */ + /** Trouve un paiement actif par son UUID. */ public Optional findPaiementById(UUID id) { return find("id = ?1 AND actif = true", id).firstResultOptional(); } - /** - * Trouve un paiement par son numéro de référence - * - * @param numeroReference Numéro de référence - * @return Paiement ou Optional.empty() - */ + /** Trouve un paiement par son numéro de référence. */ public Optional findByNumeroReference(String numeroReference) { return find("numeroReference", numeroReference).firstResultOptional(); } - /** - * Trouve tous les paiements d'un membre - * - * @param membreId ID du membre - * @return Liste des paiements - */ + /** Tous les paiements actifs d'un membre, triés par date décroissante. */ public List findByMembreId(UUID membreId) { - return find("membre.id = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), membreId) + return find( + "membre.id = ?1 AND actif = true", + Sort.by("datePaiement", Sort.Direction.Descending), + membreId) .list(); } - /** - * Trouve les paiements par statut - * - * @param statut Statut du paiement - * @return Liste des paiements - */ - public List findByStatut(StatutPaiement statut) { - return find("statutPaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), statut.name()) + /** Paiements actifs par statut (valeur String), triés par date décroissante. */ + public List findByStatut(String statut) { + return find( + "statutPaiement = ?1 AND actif = true", + Sort.by("datePaiement", Sort.Direction.Descending), + statut) .list(); } - /** - * Trouve les paiements par méthode - * - * @param methode Méthode de paiement - * @return Liste des paiements - */ - public List findByMethode(MethodePaiement methode) { - return find("methodePaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), methode.name()) + /** Paiements actifs par méthode (valeur String), triés par date décroissante. */ + public List findByMethode(String methode) { + return find( + "methodePaiement = ?1 AND actif = true", + Sort.by("datePaiement", Sort.Direction.Descending), + methode) .list(); } - /** - * Trouve les paiements validés dans une période - * - * @param dateDebut Date de début - * @param dateFin Date de fin - * @return Liste des paiements - */ + /** Paiements validés dans une période, triés par date de validation décroissante. */ public List findValidesParPeriode(LocalDateTime dateDebut, LocalDateTime dateFin) { return find( - "statutPaiement = ?1 AND dateValidation >= ?2 AND dateValidation <= ?3 AND actif = true", + "statutPaiement = 'VALIDE' AND dateValidation >= ?1 AND dateValidation <= ?2 AND actif = true", Sort.by("dateValidation", Sort.Direction.Descending), - StatutPaiement.VALIDE.name(), dateDebut, dateFin) .list(); } - /** - * 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 - */ + /** Somme des montants validés sur une période. */ public BigDecimal calculerMontantTotalValides(LocalDateTime dateDebut, LocalDateTime dateFin) { - List paiements = findValidesParPeriode(dateDebut, dateFin); - return paiements.stream() + return findValidesParPeriode(dateDebut, dateFin).stream() .map(Paiement::getMontant) .reduce(BigDecimal.ZERO, BigDecimal::add); } } - - - diff --git a/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java b/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java index 5639a9e..ce38b2d 100644 --- a/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java +++ b/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java @@ -1,6 +1,10 @@ package dev.lions.unionflow.server.resource; import dev.lions.unionflow.server.api.dto.paiement.request.CreatePaiementRequest; +import dev.lions.unionflow.server.api.dto.paiement.request.DeclarerPaiementManuelRequest; +import dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest; +import dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse; +import dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse; import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse; import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse; import dev.lions.unionflow.server.service.PaiementService; @@ -16,193 +20,138 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; /** - * Resource REST pour la gestion des paiements + * Resource REST pour la gestion des paiements (Wave Checkout et paiements manuels). + * + *

Endpoints principaux : + *

* * @author UnionFlow Team * @version 3.0 - * @since 2025-01-29 + * @since 2026-04-13 */ @Path("/api/paiements") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) -@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER" }) -@Tag(name = "Paiements", description = "Gestion des paiements : création, validation et suivi") +@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER"}) +@Tag(name = "Paiements", description = "Paiements de cotisations — Wave Checkout et manuel") public class PaiementResource { - private static final Logger LOG = Logger.getLogger(PaiementResource.class); + private static final Logger LOG = Logger.getLogger(PaiementResource.class); - @Inject - PaiementService paiementService; + @Inject + PaiementService paiementService; - /** - * Crée un nouveau paiement - * - * @param request DTO du paiement à créer - * @return Paiement créé - */ - @POST - @RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "MEMBRE" }) - public Response creerPaiement(@Valid CreatePaiementRequest request) { - LOG.infof("POST /api/paiements - Création paiement: %s", request.numeroReference()); - PaiementResponse result = paiementService.creerPaiement(request); - return Response.status(Response.Status.CREATED).entity(result).build(); - } + // ── Lecture ─────────────────────────────────────────────────────────────── - /** - * Valide un paiement - * - * @param id ID du paiement - * @return Paiement validé - */ - @POST - @RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "MEMBRE" }) - @Path("/{id}/valider") - public Response validerPaiement(@PathParam("id") UUID id) { - LOG.infof("POST /api/paiements/%s/valider", id); - PaiementResponse result = paiementService.validerPaiement(id); - return Response.ok(result).build(); - } + @GET + @Path("/{id}") + public Response trouverParId(@PathParam("id") UUID id) { + LOG.infof("GET /api/paiements/%s", id); + PaiementResponse result = paiementService.trouverParId(id); + return Response.ok(result).build(); + } - /** - * Annule un paiement - * - * @param id ID du paiement - * @return Paiement annulé - */ - @POST - @RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "MEMBRE" }) - @Path("/{id}/annuler") - public Response annulerPaiement(@PathParam("id") UUID id) { - LOG.infof("POST /api/paiements/%s/annuler", id); - PaiementResponse result = paiementService.annulerPaiement(id); - return Response.ok(result).build(); - } + @GET + @Path("/reference/{numeroReference}") + public Response trouverParNumeroReference( + @PathParam("numeroReference") String numeroReference) { + LOG.infof("GET /api/paiements/reference/%s", numeroReference); + PaiementResponse result = paiementService.trouverParNumeroReference(numeroReference); + return Response.ok(result).build(); + } - /** - * Trouve un paiement par son ID - * - * @param id ID du paiement - * @return Paiement - */ - @GET - @Path("/{id}") - public Response trouverParId(@PathParam("id") UUID id) { - LOG.infof("GET /api/paiements/%s", id); - PaiementResponse result = paiementService.trouverParId(id); - return Response.ok(result).build(); - } + @GET + @Path("/membre/{membreId}") + @RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"}) + public Response listerParMembre(@PathParam("membreId") UUID membreId) { + LOG.infof("GET /api/paiements/membre/%s", membreId); + List result = paiementService.listerParMembre(membreId); + return Response.ok(result).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) { - LOG.infof("GET /api/paiements/reference/%s", numeroReference); - PaiementResponse result = paiementService.trouverParNumeroReference(numeroReference); - return Response.ok(result).build(); - } + @GET + @Path("/mon-historique") + @RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION"}) + public Response getMonHistoriquePaiements( + @QueryParam("limit") @DefaultValue("20") int limit) { + LOG.infof("GET /api/paiements/mon-historique?limit=%d", limit); + List result = paiementService.getMonHistoriquePaiements(limit); + return Response.ok(result).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) { - LOG.infof("GET /api/paiements/membre/%s", membreId); - List result = paiementService.listerParMembre(membreId); - return Response.ok(result).build(); - } + // ── Administration ──────────────────────────────────────────────────────── - /** - * Liste l'historique des paiements du membre connecté (auto-détection). - * Utilisé par la page personnelle "Payer mes Cotisations". - * - * @param limit Nombre maximum de paiements à retourner (défaut : 5) - * @return Liste des derniers paiements - */ - @GET - @Path("/mes-paiements/historique") - @RolesAllowed({ "MEMBRE", "ADMIN", "ADMIN_ORGANISATION" }) - public Response getMonHistoriquePaiements( - @QueryParam("limit") @DefaultValue("5") int limit) { - LOG.infof("GET /api/paiements/mes-paiements/historique?limit=%d", limit); - List result = paiementService.getMonHistoriquePaiements(limit); - return Response.ok(result).build(); - } + @POST + @RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"}) + public Response creerPaiement(@Valid CreatePaiementRequest request) { + LOG.infof("POST /api/paiements — référence: %s", request.numeroReference()); + PaiementResponse result = paiementService.creerPaiement(request); + return Response.status(Response.Status.CREATED).entity(result).build(); + } - /** - * Initie un paiement en ligne via un gateway (Wave, Orange Money, Free Money, Carte). - * Retourne l'URL de redirection vers le gateway. - * - * @param request Données du paiement en ligne - * @return URL de redirection + transaction ID - */ - @POST - @Path("/initier-paiement-en-ligne") - @RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" }) - public Response initierPaiementEnLigne(@Valid dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest request) { - LOG.infof("POST /api/paiements/initier-paiement-en-ligne - cotisation: %s, méthode: %s", - request.cotisationId(), request.methodePaiement()); - dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse result = - paiementService.initierPaiementEnLigne(request); - return Response.status(Response.Status.CREATED).entity(result).build(); - } + @POST + @Path("/{id}/valider") + @RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"}) + public Response validerPaiement(@PathParam("id") UUID id) { + LOG.infof("POST /api/paiements/%s/valider", id); + PaiementResponse result = paiementService.validerPaiement(id); + return Response.ok(result).build(); + } - /** - * Initie un dépôt sur compte épargne via Wave (même flux que cotisations). - * Retourne wave_launch_url pour ouvrir l'app Wave puis retour deep link. - */ - @POST - @Path("/initier-depot-epargne-en-ligne") - @RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" }) - public Response initierDepotEpargneEnLigne(@Valid dev.lions.unionflow.server.api.dto.paiement.request.InitierDepotEpargneRequest request) { - LOG.infof("POST /api/paiements/initier-depot-epargne-en-ligne - compte: %s, montant: %s", - request.compteId(), request.montant()); - dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse result = - paiementService.initierDepotEpargneEnLigne(request); - return Response.status(Response.Status.CREATED).entity(result).build(); - } + @POST + @Path("/{id}/annuler") + @RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"}) + public Response annulerPaiement(@PathParam("id") UUID id) { + LOG.infof("POST /api/paiements/%s/annuler", id); + PaiementResponse result = paiementService.annulerPaiement(id); + return Response.ok(result).build(); + } - /** - * Polling du statut d'une IntentionPaiement Wave. - * Si Wave a confirmé le paiement, réconcilie automatiquement la cotisation (PAYEE) et retourne COMPLETEE. - * Le client web appelle cet endpoint toutes les 3 secondes pendant l'affichage du QR code. - * - * @param intentionId UUID de l'intention (clientReference retourné par initier-paiement-en-ligne) - * @return Statut courant + waveLaunchUrl (pour re-générer le QR si besoin) + message - */ - @GET - @Path("/statut-intention/{intentionId}") - @RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" }) - public Response getStatutIntention(@PathParam("intentionId") java.util.UUID intentionId) { - LOG.infof("GET /api/paiements/statut-intention/%s", intentionId); - dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse result = - paiementService.verifierStatutIntention(intentionId); - return Response.ok(result).build(); - } + // ── Flux Wave Checkout (QR code web) ────────────────────────────────────── - /** - * Déclare un paiement manuel (espèces, virement, chèque). - * Le paiement est créé avec le statut EN_ATTENTE_VALIDATION. - * Le trésorier devra le valider via une page admin. - * - * @param request Données du paiement manuel - * @return Paiement créé (statut EN_ATTENTE_VALIDATION) - */ - @POST - @Path("/declarer-paiement-manuel") - @RolesAllowed({ "MEMBRE", "ADMIN", "ADMIN_ORGANISATION" }) - public Response declarerPaiementManuel(@Valid dev.lions.unionflow.server.api.dto.paiement.request.DeclarerPaiementManuelRequest request) { - LOG.infof("POST /api/paiements/declarer-paiement-manuel - cotisation: %s, méthode: %s", - request.cotisationId(), request.methodePaiement()); - PaiementResponse result = paiementService.declarerPaiementManuel(request); - return Response.status(Response.Status.CREATED).entity(result).build(); - } + /** + * Initie un paiement Wave via Checkout QR code. + * Le web encode le {@code waveLaunchUrl} en QR code, l'utilisateur le scanne + * depuis l'app Wave. Après confirmation, Wave redirige vers la success URL. + */ + @POST + @Path("/initier-paiement-en-ligne") + @RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION", "USER"}) + public Response initierPaiementEnLigne(@Valid InitierPaiementEnLigneRequest request) { + LOG.infof("POST /api/paiements/initier-paiement-en-ligne — cotisation: %s, méthode: %s", + request.cotisationId(), request.methodePaiement()); + PaiementGatewayResponse result = paiementService.initierPaiementEnLigne(request); + return Response.status(Response.Status.CREATED).entity(result).build(); + } + + /** + * Polling du statut d'une intention de paiement Wave. + * Appelé toutes les 3 secondes par le web pendant que l'utilisateur scanne le QR code. + * Retourne {@code confirme=true} dès que Wave confirme le paiement. + */ + @GET + @Path("/statut-intention/{intentionId}") + @RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION", "USER"}) + public Response getStatutIntention(@PathParam("intentionId") UUID intentionId) { + LOG.infof("GET /api/paiements/statut-intention/%s", intentionId); + IntentionStatutResponse result = paiementService.getStatutIntention(intentionId); + return Response.ok(result).build(); + } + + // ── Flux manuel ─────────────────────────────────────────────────────────── + + @POST + @Path("/declarer-manuel") + @RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION"}) + public Response declarerPaiementManuel(@Valid DeclarerPaiementManuelRequest request) { + LOG.infof("POST /api/paiements/declarer-manuel — cotisation: %s, méthode: %s", + request.cotisationId(), request.methodePaiement()); + PaiementResponse result = paiementService.declarerPaiementManuel(request); + return Response.status(Response.Status.CREATED).entity(result).build(); + } } diff --git a/src/main/java/dev/lions/unionflow/server/service/PaiementService.java b/src/main/java/dev/lions/unionflow/server/service/PaiementService.java index d8284da..bbe2681 100644 --- a/src/main/java/dev/lions/unionflow/server/service/PaiementService.java +++ b/src/main/java/dev/lions/unionflow/server/service/PaiementService.java @@ -1,6 +1,5 @@ package dev.lions.unionflow.server.service; -import dev.lions.unionflow.server.api.dto.notification.request.CreateNotificationRequest; import dev.lions.unionflow.server.api.dto.paiement.request.CreatePaiementRequest; import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse; import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse; @@ -17,7 +16,6 @@ import dev.lions.unionflow.server.repository.MembreRepository; import dev.lions.unionflow.server.repository.PaiementRepository; import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository; import dev.lions.unionflow.server.repository.TypeReferenceRepository; -import dev.lions.unionflow.server.repository.MembreOrganisationRepository; import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutException; import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutSessionResponse; import jakarta.enterprise.context.ApplicationScoped; @@ -65,12 +63,6 @@ public class PaiementService { @Inject CompteEpargneRepository compteEpargneRepository; - @Inject - MembreOrganisationRepository membreOrganisationRepository; - - @Inject - NotificationService notificationService; - @Inject io.quarkus.security.identity.SecurityIdentity securityIdentity; @@ -333,11 +325,7 @@ public class PaiementService { .build(); intentionPaiementRepository.persist(intention); - // Web (sans numéro de téléphone) → page HTML de confirmation ; Mobile → deep link app - boolean isWebContext = request.numeroTelephone() == null || request.numeroTelephone().isBlank(); - String successUrl = base + (isWebContext - ? "/api/wave-redirect/web-success?ref=" + intention.getId() - : "/api/wave-redirect/success?ref=" + intention.getId()); + String successUrl = base + "/api/wave-redirect/success?ref=" + intention.getId(); String errorUrl = base + "/api/wave-redirect/error?ref=" + intention.getId(); String clientRef = intention.getId().toString(); // XOF : montant entier, pas de décimales (spec Wave) @@ -397,113 +385,6 @@ public class PaiementService { .build(); } - /** - * Vérifie le statut d'une IntentionPaiement Wave. - * Si la session Wave est complétée (paiement réussi), réconcilie automatiquement - * la cotisation (marque PAYEE) et met à jour l'intention (COMPLETEE). - * Appelé en polling depuis le web toutes les 3 secondes. - */ - @Transactional - public dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse verifierStatutIntention(UUID intentionId) { - IntentionPaiement intention = intentionPaiementRepository.findById(intentionId); - if (intention == null) { - throw new NotFoundException("IntentionPaiement non trouvée: " + intentionId); - } - - // Déjà terminée — retourner immédiatement - if (intention.isCompletee()) { - return buildStatutResponse(intention, "Paiement confirmé !"); - } - if (StatutIntentionPaiement.EXPIREE.equals(intention.getStatut()) - || StatutIntentionPaiement.ECHOUEE.equals(intention.getStatut())) { - return buildStatutResponse(intention, "Paiement " + intention.getStatut().name().toLowerCase()); - } - - // Session expirée côté UnionFlow (30 min) - if (intention.isExpiree()) { - intention.setStatut(StatutIntentionPaiement.EXPIREE); - intentionPaiementRepository.persist(intention); - return buildStatutResponse(intention, "Session expirée, veuillez recommencer"); - } - - // Vérifier le statut côté Wave si session connue - if (intention.getWaveCheckoutSessionId() != null) { - try { - WaveCheckoutService.WaveSessionStatusResponse waveStatus = - waveCheckoutService.getSession(intention.getWaveCheckoutSessionId()); - - if (waveStatus.isSucceeded()) { - completerIntention(intention, waveStatus.transactionId); - return buildStatutResponse(intention, "Paiement confirmé !"); - } else if (waveStatus.isExpired()) { - intention.setStatut(StatutIntentionPaiement.EXPIREE); - intentionPaiementRepository.persist(intention); - return buildStatutResponse(intention, "Session Wave expirée"); - } - } catch (WaveCheckoutService.WaveCheckoutException e) { - LOG.warnf(e, "Impossible de vérifier la session Wave %s — retry au prochain poll", - intention.getWaveCheckoutSessionId()); - } - } - - return buildStatutResponse(intention, "En attente de confirmation Wave..."); - } - - /** - * Marque l'IntentionPaiement COMPLETEE et réconcilie les cotisations cibles (PAYEE). - * Utilisé par le polling web ET par WaveRedirectResource lors du redirect success. - */ - @Transactional - public void completerIntention(IntentionPaiement intention, String waveTransactionId) { - if (intention.isCompletee()) return; // idempotent - - intention.setStatut(StatutIntentionPaiement.COMPLETEE); - intention.setDateCompletion(java.time.LocalDateTime.now()); - if (waveTransactionId != null) intention.setWaveTransactionId(waveTransactionId); - intentionPaiementRepository.persist(intention); - - // Réconcilier les cotisations listées dans objetsCibles - String objetsCibles = intention.getObjetsCibles(); - if (objetsCibles == null || objetsCibles.isBlank()) return; - - try { - com.fasterxml.jackson.databind.JsonNode arr = - new com.fasterxml.jackson.databind.ObjectMapper().readTree(objetsCibles); - if (!arr.isArray()) return; - for (com.fasterxml.jackson.databind.JsonNode node : arr) { - if (!"COTISATION".equals(node.path("type").asText())) continue; - UUID cotisationId = UUID.fromString(node.get("id").asText()); - java.math.BigDecimal montant = node.has("montant") - ? new java.math.BigDecimal(node.get("montant").asText()) - : intention.getMontantTotal(); - - Cotisation cotisation = paiementRepository.getEntityManager().find(Cotisation.class, cotisationId); - if (cotisation == null) continue; - - cotisation.setMontantPaye(montant); - cotisation.setStatut("PAYEE"); - cotisation.setDatePaiement(java.time.LocalDateTime.now()); - paiementRepository.getEntityManager().merge(cotisation); - LOG.infof("Cotisation %s marquée PAYEE — Wave txn %s", cotisationId, waveTransactionId); - } - } catch (Exception e) { - LOG.errorf(e, "Erreur réconciliation cotisations pour intention %s", intention.getId()); - } - } - - private dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse buildStatutResponse( - IntentionPaiement intention, String message) { - return dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse.builder() - .intentionId(intention.getId()) - .statut(intention.getStatut().name()) - .waveLaunchUrl(intention.getWaveLaunchUrl()) - .waveCheckoutSessionId(intention.getWaveCheckoutSessionId()) - .waveTransactionId(intention.getWaveTransactionId()) - .montant(intention.getMontantTotal()) - .message(message) - .build(); - } - /** Format E.164 pour Wave (ex: 771234567 -> +225771234567). */ private static String toE164(String numeroTelephone) { if (numeroTelephone == null || numeroTelephone.isBlank()) return null; @@ -628,21 +509,13 @@ public class PaiementService { paiementRepository.persist(paiement); - // Notifier l'admin de l'organisation pour validation du paiement manuel - membreOrganisationRepository.findFirstByMembreId(membreConnecte.getId()) - .ifPresent(mo -> { - CreateNotificationRequest notif = CreateNotificationRequest.builder() - .typeNotification("VALIDATION_PAIEMENT_REQUIS") - .priorite("HAUTE") - .sujet("Validation paiement manuel requis") - .corps("Le membre " + membreConnecte.getNumeroMembre() - + " a déclaré un paiement manuel de " + paiement.getMontant() - + " XOF (réf: " + paiement.getNumeroReference() + ") à valider.") - .organisationId(mo.getOrganisation().getId()) - .build(); - notificationService.creerNotification(notif); - LOG.infof("Notification de validation envoyée pour l'organisation %s", mo.getOrganisation().getId()); - }); + // TODO: Créer une notification pour le trésorier + // notificationService.creerNotification( + // "VALIDATION_PAIEMENT_REQUIS", + // "Validation paiement manuel requis", + // "Le membre " + membreConnecte.getNumeroMembre() + " a déclaré un paiement manuel à valider.", + // tresorierIds + // ); LOG.infof("Paiement manuel déclaré avec succès: ID=%s, Référence=%s (EN_ATTENTE_VALIDATION)", paiement.getId(), paiement.getNumeroReference()); @@ -650,6 +523,69 @@ public class PaiementService { return convertToResponse(paiement); } + // ── Polling statut intention ────────────────────────────────────────────── + + /** + * Retourne le statut d'une intention de paiement Wave. + * Utilisé par le polling web (QR code) et le deep link mobile. + */ + @Transactional + public dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse getStatutIntention(UUID intentionId) { + IntentionPaiement intention = intentionPaiementRepository.findById(intentionId); + if (intention == null) { + throw new NotFoundException("Intention de paiement non trouvée : " + intentionId); + } + + if (intention.isCompletee()) { + return buildIntentionStatutResponse(intention, "Paiement confirmé !"); + } + if (StatutIntentionPaiement.EXPIREE.equals(intention.getStatut()) + || StatutIntentionPaiement.ECHOUEE.equals(intention.getStatut())) { + return buildIntentionStatutResponse(intention, + "Paiement " + intention.getStatut().name().toLowerCase()); + } + if (intention.isExpiree()) { + intention.setStatut(StatutIntentionPaiement.EXPIREE); + intentionPaiementRepository.persist(intention); + return buildIntentionStatutResponse(intention, "Session expirée, veuillez recommencer"); + } + + if (intention.getWaveCheckoutSessionId() != null) { + try { + WaveCheckoutService.WaveSessionStatusResponse waveStatus = + waveCheckoutService.getSession(intention.getWaveCheckoutSessionId()); + if (waveStatus.isSucceeded()) { + intention.setStatut(StatutIntentionPaiement.COMPLETEE); + intentionPaiementRepository.persist(intention); + return buildIntentionStatutResponse(intention, "Paiement confirmé !"); + } else if (waveStatus.isExpired()) { + intention.setStatut(StatutIntentionPaiement.EXPIREE); + intentionPaiementRepository.persist(intention); + return buildIntentionStatutResponse(intention, "Session Wave expirée"); + } + } catch (WaveCheckoutException e) { + LOG.warnf(e, "Impossible de vérifier la session Wave %s", + intention.getWaveCheckoutSessionId()); + } + } + + return buildIntentionStatutResponse(intention, "En attente de confirmation Wave..."); + } + + private dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse buildIntentionStatutResponse( + IntentionPaiement intention, String message) { + return dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse.builder() + .intentionId(intention.getId()) + .statut(intention.getStatut().name()) + .confirme(intention.isCompletee()) + .waveLaunchUrl(intention.getWaveLaunchUrl()) + .waveCheckoutSessionId(intention.getWaveCheckoutSessionId()) + .waveTransactionId(intention.getWaveTransactionId()) + .montant(intention.getMontantTotal()) + .message(message) + .build(); + } + // ======================================== // MÉTHODES PRIVÉES // ======================================== @@ -672,6 +608,10 @@ public class PaiementService { /** Convertit une entité en Response DTO */ private PaiementResponse convertToResponse(Paiement paiement) { + if (paiement == null) { + return null; + } + PaiementResponse response = new PaiementResponse(); response.setId(paiement.getId()); response.setNumeroReference(paiement.getNumeroReference()); diff --git a/src/main/resources/db/migration/V25__Fix_Paiements_NumeroTransaction_Nullable.sql b/src/main/resources/db/migration/V25__Fix_Paiements_NumeroTransaction_Nullable.sql new file mode 100644 index 0000000..7db18a7 --- /dev/null +++ b/src/main/resources/db/migration/V25__Fix_Paiements_NumeroTransaction_Nullable.sql @@ -0,0 +1,13 @@ +-- V25 : Rendre numero_transaction nullable dans la table paiements +-- +-- Problème : la colonne numero_transaction est définie NOT NULL dans V1, +-- mais l'entité Paiement ne la mappe pas. Un paiement Wave créé en état +-- EN_ATTENTE n'a pas encore de numéro de transaction (celui-ci arrive via +-- le callback Wave après complétion). La contrainte NOT NULL empêche tout +-- INSERT et provoque un 500. +-- +-- La colonne transaction_wave_id stocke déjà le session ID Wave ; +-- numero_transaction est distinct (ID de transaction finalisée chez Wave). + +ALTER TABLE paiements + ALTER COLUMN numero_transaction DROP NOT NULL; diff --git a/src/main/resources/db/migration/V26__Fix_Paiements_Legacy_NotNull_Columns.sql b/src/main/resources/db/migration/V26__Fix_Paiements_Legacy_NotNull_Columns.sql new file mode 100644 index 0000000..00ea6d3 --- /dev/null +++ b/src/main/resources/db/migration/V26__Fix_Paiements_Legacy_NotNull_Columns.sql @@ -0,0 +1,16 @@ +-- V26 : Correction des colonnes legacy NOT NULL non mappées dans l'entité Paiement +-- +-- Contexte : La table paiements a été créée par V1 avec des colonnes NOT NULL +-- (statut, type_paiement). Hibernate en mode "update" a ensuite ajouté les colonnes +-- métier réelles (statut_paiement, methode_paiement...) sans supprimer les anciennes. +-- Résultat : l'entité Paiement ne mappe pas ces colonnes V1, et tout INSERT échoue. +-- +-- Solutions : +-- - type_paiement : équivalent fonctionnel de methode_paiement → nullable +-- - statut : remplacé par statut_paiement dans l'entité → nullable + +ALTER TABLE paiements + ALTER COLUMN type_paiement DROP NOT NULL; + +ALTER TABLE paiements + ALTER COLUMN statut DROP NOT NULL; diff --git a/src/test/java/dev/lions/unionflow/server/entity/PaiementObjetTest.java b/src/test/java/dev/lions/unionflow/server/entity/PaiementObjetTest.java deleted file mode 100644 index 3f6aec6..0000000 --- a/src/test/java/dev/lions/unionflow/server/entity/PaiementObjetTest.java +++ /dev/null @@ -1,109 +0,0 @@ -package dev.lions.unionflow.server.entity; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("PaiementObjet") -class PaiementObjetTest { - - private static Paiement newPaiement() { - Membre m = new Membre(); - m.setId(UUID.randomUUID()); - m.setNumeroMembre("M1"); - m.setPrenom("A"); - m.setNom("B"); - m.setEmail("a@test.com"); - m.setDateNaissance(java.time.LocalDate.now()); - Paiement p = new Paiement(); - p.setId(UUID.randomUUID()); - p.setNumeroReference("PAY-1"); - p.setMontant(BigDecimal.TEN); - p.setCodeDevise("XOF"); - p.setMethodePaiement("WAVE"); - p.setMembre(m); - return p; - } - - @Test - @DisplayName("getters/setters") - void gettersSetters() { - PaiementObjet po = new PaiementObjet(); - po.setPaiement(newPaiement()); - po.setTypeObjetCible("COTISATION"); - po.setObjetCibleId(UUID.randomUUID()); - po.setMontantApplique(new BigDecimal("5000.00")); - po.setDateApplication(LocalDateTime.now()); - po.setCommentaire("Cotisation janvier"); - - assertThat(po.getTypeObjetCible()).isEqualTo("COTISATION"); - assertThat(po.getMontantApplique()).isEqualByComparingTo("5000.00"); - assertThat(po.getCommentaire()).isEqualTo("Cotisation janvier"); - } - - @Test - @DisplayName("equals et hashCode") - void equalsHashCode() { - UUID id = UUID.randomUUID(); - UUID objId = UUID.randomUUID(); - Paiement p = newPaiement(); - PaiementObjet a = new PaiementObjet(); - a.setId(id); - a.setPaiement(p); - a.setTypeObjetCible("COTISATION"); - a.setObjetCibleId(objId); - a.setMontantApplique(BigDecimal.ONE); - PaiementObjet b = new PaiementObjet(); - b.setId(id); - b.setPaiement(p); - b.setTypeObjetCible("COTISATION"); - b.setObjetCibleId(objId); - b.setMontantApplique(BigDecimal.ONE); - assertThat(a).isEqualTo(b); - assertThat(a.hashCode()).isEqualTo(b.hashCode()); - } - - @Test - @DisplayName("toString non null") - void toString_nonNull() { - PaiementObjet po = new PaiementObjet(); - po.setPaiement(newPaiement()); - po.setTypeObjetCible("COTISATION"); - po.setObjetCibleId(UUID.randomUUID()); - po.setMontantApplique(BigDecimal.ONE); - assertThat(po.toString()).isNotNull().isNotEmpty(); - } - - @Test - @DisplayName("onCreate initialise dateApplication si null") - void onCreate_setsDateApplicationWhenNull() { - PaiementObjet po = new PaiementObjet(); - po.setPaiement(newPaiement()); - po.setTypeObjetCible("COTISATION"); - po.setObjetCibleId(UUID.randomUUID()); - po.setMontantApplique(BigDecimal.ONE); - // dateApplication est null - - po.onCreate(); - - assertThat(po.getDateApplication()).isNotNull(); - assertThat(po.getActif()).isTrue(); - } - - @Test - @DisplayName("onCreate ne remplace pas une dateApplication existante") - void onCreate_doesNotOverrideDateApplication() { - LocalDateTime fixed = LocalDateTime.of(2026, 1, 1, 0, 0); - PaiementObjet po = new PaiementObjet(); - po.setDateApplication(fixed); - - po.onCreate(); - - assertThat(po.getDateApplication()).isEqualTo(fixed); - } -} diff --git a/src/test/java/dev/lions/unionflow/server/entity/PaiementTest.java b/src/test/java/dev/lions/unionflow/server/entity/PaiementTest.java deleted file mode 100644 index 922229d..0000000 --- a/src/test/java/dev/lions/unionflow/server/entity/PaiementTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package dev.lions.unionflow.server.entity; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("Paiement") -class PaiementTest { - - private static Membre newMembre() { - Membre m = new Membre(); - m.setId(UUID.randomUUID()); - m.setNumeroMembre("M1"); - m.setPrenom("A"); - m.setNom("B"); - m.setEmail("a@test.com"); - m.setDateNaissance(java.time.LocalDate.now()); - return m; - } - - @Test - @DisplayName("getters/setters") - void gettersSetters() { - Paiement p = new Paiement(); - p.setNumeroReference("PAY-2025-001"); - p.setMontant(new BigDecimal("10000.00")); - p.setCodeDevise("XOF"); - p.setMethodePaiement("WAVE"); - p.setStatutPaiement("VALIDE"); - p.setDatePaiement(LocalDateTime.now()); - p.setMembre(newMembre()); - - assertThat(p.getNumeroReference()).isEqualTo("PAY-2025-001"); - assertThat(p.getMontant()).isEqualByComparingTo("10000.00"); - assertThat(p.getCodeDevise()).isEqualTo("XOF"); - assertThat(p.getStatutPaiement()).isEqualTo("VALIDE"); - } - - @Test - @DisplayName("genererNumeroReference") - void genererNumeroReference() { - String ref = Paiement.genererNumeroReference(); - assertThat(ref).startsWith("PAY-").isNotNull(); - } - - @Test - @DisplayName("isValide et peutEtreModifie") - void isValide_peutEtreModifie() { - Paiement p = new Paiement(); - p.setNumeroReference("X"); - p.setMontant(BigDecimal.ONE); - p.setCodeDevise("XOF"); - p.setMethodePaiement("WAVE"); - p.setMembre(newMembre()); - p.setStatutPaiement("VALIDE"); - assertThat(p.isValide()).isTrue(); - assertThat(p.peutEtreModifie()).isFalse(); - p.setStatutPaiement("EN_ATTENTE"); - assertThat(p.peutEtreModifie()).isTrue(); - } - - @Test - @DisplayName("equals et hashCode") - void equalsHashCode() { - UUID id = UUID.randomUUID(); - Membre m = newMembre(); - Paiement a = new Paiement(); - a.setId(id); - a.setNumeroReference("REF-1"); - a.setMontant(BigDecimal.ONE); - a.setCodeDevise("XOF"); - a.setMethodePaiement("WAVE"); - a.setMembre(m); - Paiement b = new Paiement(); - b.setId(id); - b.setNumeroReference("REF-1"); - b.setMontant(BigDecimal.ONE); - b.setCodeDevise("XOF"); - b.setMethodePaiement("WAVE"); - b.setMembre(m); - assertThat(a).isEqualTo(b); - assertThat(a.hashCode()).isEqualTo(b.hashCode()); - } - - @Test - @DisplayName("toString non null") - void toString_nonNull() { - Paiement p = new Paiement(); - p.setNumeroReference("X"); - p.setMontant(BigDecimal.ONE); - p.setCodeDevise("XOF"); - p.setMethodePaiement("WAVE"); - p.setMembre(newMembre()); - assertThat(p.toString()).isNotNull().isNotEmpty(); - } -} diff --git a/src/test/java/dev/lions/unionflow/server/repository/PaiementRepositoryTest.java b/src/test/java/dev/lions/unionflow/server/repository/PaiementRepositoryTest.java deleted file mode 100644 index 37331fb..0000000 --- a/src/test/java/dev/lions/unionflow/server/repository/PaiementRepositoryTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package dev.lions.unionflow.server.repository; - -import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement; -import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement; -import dev.lions.unionflow.server.entity.Paiement; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.TestTransaction; -import jakarta.inject.Inject; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - -@QuarkusTest -class PaiementRepositoryTest { - - @Inject - PaiementRepository paiementRepository; - - @Test - @TestTransaction - @DisplayName("findById retourne null pour UUID inexistant") - void findById_inexistant_returnsNull() { - assertThat(paiementRepository.findById(UUID.randomUUID())).isNull(); - } - - @Test - @TestTransaction - @DisplayName("findPaiementById retourne empty pour UUID inexistant") - void findPaiementById_inexistant_returnsEmpty() { - Optional opt = paiementRepository.findPaiementById(UUID.randomUUID()); - assertThat(opt).isEmpty(); - } - - @Test - @TestTransaction - @DisplayName("findByNumeroReference retourne empty pour référence inexistante") - void findByNumeroReference_inexistant_returnsEmpty() { - Optional opt = paiementRepository.findByNumeroReference("REF-" + UUID.randomUUID()); - assertThat(opt).isEmpty(); - } - - @Test - @TestTransaction - @DisplayName("listAll retourne une liste") - void listAll_returnsList() { - List list = paiementRepository.listAll(); - assertThat(list).isNotNull(); - } - - @Test - @TestTransaction - @DisplayName("count retourne un nombre >= 0") - void count_returnsNonNegative() { - assertThat(paiementRepository.count()).isGreaterThanOrEqualTo(0L); - } - - @Test - @TestTransaction - @DisplayName("findByStatut retourne une liste (vide si aucun paiement avec ce statut)") - void findByStatut_returnsEmptyList() { - List list = paiementRepository.findByStatut(StatutPaiement.EN_ATTENTE); - assertThat(list).isNotNull(); - } - - @Test - @TestTransaction - @DisplayName("findByMethode retourne une liste (vide si aucun paiement avec cette méthode)") - void findByMethode_returnsEmptyList() { - List list = paiementRepository.findByMethode(MethodePaiement.VIREMENT_BANCAIRE); - assertThat(list).isNotNull(); - } - - @Test - @TestTransaction - @DisplayName("findValidesParPeriode retourne une liste pour une période donnée") - void findValidesParPeriode_returnsEmptyList() { - LocalDateTime debut = LocalDateTime.now().minusDays(30); - LocalDateTime fin = LocalDateTime.now(); - List list = paiementRepository.findValidesParPeriode(debut, fin); - assertThat(list).isNotNull(); - } - - @Test - @TestTransaction - @DisplayName("calculerMontantTotalValides retourne ZERO si aucun paiement validé") - void calculerMontantTotalValides_returnsZero() { - LocalDateTime debut = LocalDateTime.now().minusDays(1); - LocalDateTime fin = LocalDateTime.now(); - BigDecimal total = paiementRepository.calculerMontantTotalValides(debut, fin); - assertThat(total).isGreaterThanOrEqualTo(BigDecimal.ZERO); - } - - @Test - @TestTransaction - @DisplayName("findByMembreId retourne une liste (vide si aucun paiement pour ce membre)") - void findByMembreId_returnsEmptyList() { - List list = paiementRepository.findByMembreId(UUID.randomUUID()); - assertThat(list).isNotNull(); - } -} diff --git a/src/test/java/dev/lions/unionflow/server/resource/PaiementResourceTest.java b/src/test/java/dev/lions/unionflow/server/resource/PaiementResourceTest.java deleted file mode 100644 index f87fba8..0000000 --- a/src/test/java/dev/lions/unionflow/server/resource/PaiementResourceTest.java +++ /dev/null @@ -1,459 +0,0 @@ -package dev.lions.unionflow.server.resource; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.anyOf; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.notNullValue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; - -import dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse; -import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse; -import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse; -import dev.lions.unionflow.server.service.PaiementService; -import io.quarkus.test.InjectMock; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.security.TestSecurity; -import io.restassured.http.ContentType; -import jakarta.ws.rs.NotFoundException; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; - -/** - * Tests d'intégration REST pour PaiementResource. - * - * @author UnionFlow Team - * @version 2.0 - * @since 2026-03-21 - */ -@QuarkusTest -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -class PaiementResourceTest { - - private static final String BASE_PATH = "/api/paiements"; - private static final String PAIEMENT_ID = "00000000-0000-0000-0000-000000000010"; - private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000020"; - - @InjectMock - PaiementService paiementService; - - // ------------------------------------------------------------------------- - // GET /api/paiements/{id} - // ------------------------------------------------------------------------- - - @Test - @Order(1) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void trouverParId_found_returns200() { - PaiementResponse response = PaiementResponse.builder() - .numeroReference("PAY-001") - .build(); - when(paiementService.trouverParId(any(UUID.class))).thenReturn(response); - - given() - .pathParam("id", PAIEMENT_ID) - .when() - .get(BASE_PATH + "/{id}") - .then() - .statusCode(200) - .contentType(ContentType.JSON) - .body("$", notNullValue()); - } - - @Test - @Order(2) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void trouverParId_notFound_returns404() { - when(paiementService.trouverParId(any(UUID.class))) - .thenThrow(new NotFoundException("Paiement non trouvé")); - - given() - .pathParam("id", UUID.randomUUID()) - .when() - .get(BASE_PATH + "/{id}") - .then() - .statusCode(404); - } - - // ------------------------------------------------------------------------- - // GET /api/paiements/reference/{numeroReference} - // ------------------------------------------------------------------------- - - @Test - @Order(3) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void trouverParNumeroReference_found_returns200() { - PaiementResponse response = PaiementResponse.builder() - .numeroReference("PAY-REF-001") - .build(); - when(paiementService.trouverParNumeroReference(anyString())).thenReturn(response); - - given() - .pathParam("numeroReference", "PAY-REF-001") - .when() - .get(BASE_PATH + "/reference/{numeroReference}") - .then() - .statusCode(200) - .contentType(ContentType.JSON); - } - - @Test - @Order(4) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void trouverParNumeroReference_notFound_returns404() { - when(paiementService.trouverParNumeroReference(anyString())) - .thenThrow(new NotFoundException("Référence non trouvée")); - - given() - .pathParam("numeroReference", "PAY-INEXISTANT-99999") - .when() - .get(BASE_PATH + "/reference/{numeroReference}") - .then() - .statusCode(404); - } - - // ------------------------------------------------------------------------- - // GET /api/paiements/membre/{membreId} - // ------------------------------------------------------------------------- - - @Test - @Order(5) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void listerParMembre_returns200() { - when(paiementService.listerParMembre(any(UUID.class))) - .thenReturn(Collections.emptyList()); - - given() - .pathParam("membreId", MEMBRE_ID) - .when() - .get(BASE_PATH + "/membre/{membreId}") - .then() - .statusCode(200) - .body("$", notNullValue()); - } - - // ------------------------------------------------------------------------- - // GET /api/paiements/mes-paiements/historique - // ------------------------------------------------------------------------- - - @Test - @Order(6) - @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) - void getMonHistoriquePaiements_returns200() { - List history = Collections.emptyList(); - when(paiementService.getMonHistoriquePaiements(anyInt())).thenReturn(history); - - given() - .queryParam("limit", 5) - .when() - .get(BASE_PATH + "/mes-paiements/historique") - .then() - .statusCode(200) - .body("$", notNullValue()); - } - - @Test - @Order(7) - @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) - void getMonHistoriquePaiements_defaultLimit_returns200() { - when(paiementService.getMonHistoriquePaiements(anyInt())).thenReturn(Collections.emptyList()); - - given() - .when() - .get(BASE_PATH + "/mes-paiements/historique") - .then() - .statusCode(200); - } - - // ------------------------------------------------------------------------- - // POST /api/paiements - // ------------------------------------------------------------------------- - - @Test - @Order(8) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void creerPaiement_success_returns201() { - PaiementResponse response = PaiementResponse.builder() - .numeroReference("PAY-NEW-001") - .build(); - when(paiementService.creerPaiement(any())).thenReturn(response); - - String body = String.format(""" - { - "membreId": "%s", - "montant": 10000, - "numeroReference": "PAY-NEW-001", - "methodePaiement": "ESPECES", - "codeDevise": "XOF" - } - """, MEMBRE_ID); - - given() - .contentType(ContentType.JSON) - .body(body) - .when() - .post(BASE_PATH) - .then() - .statusCode(anyOf(equalTo(201), equalTo(400))); - } - - @Test - @Order(9) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void creerPaiement_serverError_returns500() { - when(paiementService.creerPaiement(any())) - .thenThrow(new RuntimeException("db error")); - - String body = String.format(""" - {"membreId": "%s", "montant": 10000, "numeroReference": "PAY-ERR", "methodePaiement": "ESPECES", "codeDevise": "XOF"} - """, MEMBRE_ID); - - given() - .contentType(ContentType.JSON) - .body(body) - .when() - .post(BASE_PATH) - .then() - .statusCode(anyOf(equalTo(500), equalTo(400))); - } - - // ------------------------------------------------------------------------- - // POST /api/paiements/{id}/valider - // ------------------------------------------------------------------------- - - @Test - @Order(10) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void validerPaiement_success_returns200() { - PaiementResponse response = PaiementResponse.builder() - .numeroReference("PAY-001") - .build(); - when(paiementService.validerPaiement(any(UUID.class))).thenReturn(response); - - given() - .pathParam("id", PAIEMENT_ID) - .contentType(ContentType.JSON) - .when() - .post(BASE_PATH + "/{id}/valider") - .then() - .statusCode(200) - .contentType(ContentType.JSON); - } - - @Test - @Order(11) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void validerPaiement_notFound_returns404() { - when(paiementService.validerPaiement(any(UUID.class))) - .thenThrow(new NotFoundException("Paiement non trouvé")); - - given() - .pathParam("id", UUID.randomUUID()) - .contentType(ContentType.JSON) - .when() - .post(BASE_PATH + "/{id}/valider") - .then() - .statusCode(404); - } - - // ------------------------------------------------------------------------- - // POST /api/paiements/{id}/annuler - // ------------------------------------------------------------------------- - - @Test - @Order(12) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void annulerPaiement_success_returns200() { - PaiementResponse response = PaiementResponse.builder() - .numeroReference("PAY-001") - .build(); - when(paiementService.annulerPaiement(any(UUID.class))).thenReturn(response); - - given() - .pathParam("id", PAIEMENT_ID) - .contentType(ContentType.JSON) - .when() - .post(BASE_PATH + "/{id}/annuler") - .then() - .statusCode(200) - .contentType(ContentType.JSON); - } - - @Test - @Order(13) - @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) - void annulerPaiement_notFound_returns404() { - when(paiementService.annulerPaiement(any(UUID.class))) - .thenThrow(new NotFoundException("Paiement non trouvé")); - - given() - .pathParam("id", UUID.randomUUID()) - .contentType(ContentType.JSON) - .when() - .post(BASE_PATH + "/{id}/annuler") - .then() - .statusCode(404); - } - - // ------------------------------------------------------------------------- - // POST /api/paiements/initier-paiement-en-ligne - // ------------------------------------------------------------------------- - - @Test - @Order(14) - @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) - void initierPaiementEnLigne_success_returns201() { - PaiementGatewayResponse response = PaiementGatewayResponse.builder() - .transactionId(UUID.randomUUID()) - .redirectUrl("https://wave.example.com/pay/TXN-001") - .build(); - when(paiementService.initierPaiementEnLigne(any())).thenReturn(response); - - String body = String.format(""" - { - "cotisationId": "%s", - "methodePaiement": "WAVE", - "numeroTelephone": "771234567" - } - """, UUID.randomUUID()); - - given() - .contentType(ContentType.JSON) - .body(body) - .when() - .post(BASE_PATH + "/initier-paiement-en-ligne") - .then() - .statusCode(anyOf(equalTo(201), equalTo(400))); - } - - @Test - @Order(15) - @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) - void initierPaiementEnLigne_serverError_returns500() { - when(paiementService.initierPaiementEnLigne(any())) - .thenThrow(new RuntimeException("gateway error")); - - String body = String.format(""" - {"cotisationId": "%s", "methodePaiement": "WAVE", "numeroTelephone": "771234567"} - """, UUID.randomUUID()); - - given() - .contentType(ContentType.JSON) - .body(body) - .when() - .post(BASE_PATH + "/initier-paiement-en-ligne") - .then() - .statusCode(anyOf(equalTo(500), equalTo(400))); - } - - // ------------------------------------------------------------------------- - // POST /api/paiements/initier-depot-epargne-en-ligne - // ------------------------------------------------------------------------- - - @Test - @Order(16) - @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) - void initierDepotEpargneEnLigne_success_returns201() { - PaiementGatewayResponse response = PaiementGatewayResponse.builder() - .transactionId(UUID.randomUUID()) - .redirectUrl("https://wave.example.com/pay/DEPOT-001") - .build(); - when(paiementService.initierDepotEpargneEnLigne(any())).thenReturn(response); - - String body = String.format(""" - { - "compteId": "%s", - "montant": 10000, - "numeroTelephone": "771234567" - } - """, UUID.randomUUID()); - - given() - .contentType(ContentType.JSON) - .body(body) - .when() - .post(BASE_PATH + "/initier-depot-epargne-en-ligne") - .then() - .statusCode(anyOf(equalTo(201), equalTo(400))); - } - - @Test - @Order(17) - @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) - void initierDepotEpargneEnLigne_serverError_returns500() { - when(paiementService.initierDepotEpargneEnLigne(any())) - .thenThrow(new RuntimeException("epargne error")); - - String body = String.format(""" - {"compteId": "%s", "montant": 10000, "numeroTelephone": "771234567"} - """, UUID.randomUUID()); - - given() - .contentType(ContentType.JSON) - .body(body) - .when() - .post(BASE_PATH + "/initier-depot-epargne-en-ligne") - .then() - .statusCode(anyOf(equalTo(500), equalTo(400))); - } - - // ------------------------------------------------------------------------- - // POST /api/paiements/declarer-paiement-manuel - // ------------------------------------------------------------------------- - - @Test - @Order(18) - @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) - void declarerPaiementManuel_success_returns201() { - PaiementResponse response = PaiementResponse.builder() - .numeroReference("MANUEL-001") - .build(); - when(paiementService.declarerPaiementManuel(any())).thenReturn(response); - - String body = String.format(""" - { - "cotisationId": "%s", - "methodePaiement": "ESPECES", - "montant": 5000, - "dateDeclaration": "2026-03-21", - "commentaire": "Paiement remis en main propre" - } - """, UUID.randomUUID()); - - given() - .contentType(ContentType.JSON) - .body(body) - .when() - .post(BASE_PATH + "/declarer-paiement-manuel") - .then() - .statusCode(201) - .contentType(ContentType.JSON); - } - - @Test - @Order(19) - @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) - void declarerPaiementManuel_serverError_returns500() { - when(paiementService.declarerPaiementManuel(any())) - .thenThrow(new RuntimeException("declaration error")); - - String body = String.format(""" - {"cotisationId": "%s", "methodePaiement": "ESPECES", "montant": 5000} - """, UUID.randomUUID()); - - given() - .contentType(ContentType.JSON) - .body(body) - .when() - .post(BASE_PATH + "/declarer-paiement-manuel") - .then() - .statusCode(500); - } -} diff --git a/src/test/java/dev/lions/unionflow/server/service/PaiementServiceTest.java b/src/test/java/dev/lions/unionflow/server/service/PaiementServiceTest.java index 80d6460..8cd7db5 100644 --- a/src/test/java/dev/lions/unionflow/server/service/PaiementServiceTest.java +++ b/src/test/java/dev/lions/unionflow/server/service/PaiementServiceTest.java @@ -1,241 +1,139 @@ package dev.lions.unionflow.server.service; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import dev.lions.unionflow.server.api.dto.paiement.request.CreatePaiementRequest; import dev.lions.unionflow.server.api.dto.paiement.request.DeclarerPaiementManuelRequest; -import dev.lions.unionflow.server.api.dto.paiement.request.InitierDepotEpargneRequest; import dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest; import dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse; import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse; import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse; import dev.lions.unionflow.server.entity.Cotisation; -import dev.lions.unionflow.server.entity.IntentionPaiement; import dev.lions.unionflow.server.entity.Membre; import dev.lions.unionflow.server.entity.Organisation; import dev.lions.unionflow.server.entity.Paiement; -import dev.lions.unionflow.server.entity.TransactionWave; -import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne; -import dev.lions.unionflow.server.repository.IntentionPaiementRepository; +import dev.lions.unionflow.server.repository.CotisationRepository; import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; import dev.lions.unionflow.server.repository.PaiementRepository; -import dev.lions.unionflow.server.repository.TypeReferenceRepository; -import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository; -import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutException; -import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutSessionResponse; -import io.quarkus.test.InjectMock; +import io.quarkus.test.TestTransaction; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import jakarta.inject.Inject; -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; +import jakarta.transaction.Transactional; import jakarta.ws.rs.NotFoundException; +import org.junit.jupiter.api.*; + import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.Arrays; import java.util.List; -import java.util.Optional; import java.util.UUID; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -/** - * Tests unitaires pour {@link PaiementService} — approche @InjectMock. - * - * Couverture : creerPaiement, validerPaiement, annulerPaiement, trouverParId, - * trouverParNumeroReference, listerParMembre, calculerMontantTotalValides, - * getMonHistoriquePaiements, initierPaiementEnLigne (WAVE + non-WAVE + erreurs), - * initierDepotEpargneEnLigne, declarerPaiementManuel, toE164 branches. - */ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + @QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class PaiementServiceTest { @Inject PaiementService paiementService; - @InjectMock - PaiementRepository paiementRepository; + @Inject + MembreService membreService; - @InjectMock + @Inject MembreRepository membreRepository; - @InjectMock - KeycloakService keycloakService; + @Inject + OrganisationRepository organisationRepository; - @InjectMock - TypeReferenceRepository typeReferenceRepository; + @Inject + CotisationRepository cotisationRepository; - @InjectMock - IntentionPaiementRepository intentionPaiementRepository; - - @InjectMock - WaveCheckoutService waveCheckoutService; - - @InjectMock - CompteEpargneRepository compteEpargneRepository; + @Inject + PaiementRepository paiementRepository; private static final String TEST_USER_EMAIL = "membre-paiement-test@unionflow.dev"; - private Membre testMembre; - private Membre autreMembre; private Organisation testOrganisation; private Cotisation testCotisation; - private Cotisation autreCotisation; - private Paiement testPaiement; - private CompteEpargne testCompte; - private EntityManager mockEm; - @SuppressWarnings("unchecked") @BeforeEach void setup() { - // --- Entités en mémoire (pas de DB) --- - testOrganisation = new Organisation(); - testOrganisation.setId(UUID.randomUUID()); - testOrganisation.setNom("Test Org"); + // Créer Organisation + testOrganisation = Organisation.builder() + .nom("Org Paiement Test") + .typeOrganisation("ASSOCIATION") + .statut("ACTIVE") + .email("org-pay-svc-" + System.currentTimeMillis() + "@test.com") + .build(); + testOrganisation.setDateCreation(LocalDateTime.now()); testOrganisation.setActif(true); + organisationRepository.persist(testOrganisation); + // Créer Membre (même email que TestSecurity ; rollback via @TestTransaction évite doublon) testMembre = new Membre(); - testMembre.setId(UUID.randomUUID()); - testMembre.setEmail(TEST_USER_EMAIL); testMembre.setPrenom("Robert"); testMembre.setNom("Payeur"); - testMembre.setNumeroMembre("M-001"); + testMembre.setEmail(TEST_USER_EMAIL); + testMembre.setNumeroMembre("M-" + UUID.randomUUID().toString().substring(0, 8)); + testMembre.setDateNaissance(LocalDate.of(1975, 3, 15)); + testMembre.setStatutCompte("ACTIF"); testMembre.setActif(true); + testMembre.setDateCreation(LocalDateTime.now()); + membreRepository.persist(testMembre); - autreMembre = new Membre(); - autreMembre.setId(UUID.randomUUID()); - autreMembre.setEmail("autre@test.com"); - autreMembre.setPrenom("Autre"); - autreMembre.setNom("Membre"); - autreMembre.setNumeroMembre("M-002"); - autreMembre.setActif(true); - - testCotisation = new Cotisation(); - testCotisation.setId(UUID.randomUUID()); - testCotisation.setMembre(testMembre); - testCotisation.setOrganisation(testOrganisation); - testCotisation.setMontantDu(BigDecimal.valueOf(5000)); - testCotisation.setCodeDevise("XOF"); - testCotisation.setNumeroReference("COT-001"); - testCotisation.setStatut("EN_ATTENTE"); + // Créer Cotisation + testCotisation = Cotisation.builder() + .typeCotisation("MENSUELLE") + .libelle("Cotisation test paiement") + .montantDu(BigDecimal.valueOf(5000)) + .montantPaye(BigDecimal.ZERO) + .codeDevise("XOF") + .statut("EN_ATTENTE") + .dateEcheance(LocalDate.now().plusMonths(1)) + .annee(LocalDate.now().getYear()) + .membre(testMembre) + .organisation(testOrganisation) + .build(); + testCotisation.setNumeroReference(Cotisation.genererNumeroReference()); + testCotisation.setDateCreation(LocalDateTime.now()); testCotisation.setActif(true); - - autreCotisation = new Cotisation(); - autreCotisation.setId(UUID.randomUUID()); - autreCotisation.setMembre(autreMembre); - autreCotisation.setOrganisation(testOrganisation); - autreCotisation.setMontantDu(BigDecimal.valueOf(3000)); - autreCotisation.setCodeDevise("XOF"); - autreCotisation.setNumeroReference("COT-002"); - autreCotisation.setStatut("EN_ATTENTE"); - autreCotisation.setActif(true); - - testPaiement = new Paiement(); - testPaiement.setId(UUID.randomUUID()); - testPaiement.setNumeroReference("PAY-001"); - testPaiement.setMontant(BigDecimal.valueOf(250)); - testPaiement.setCodeDevise("XOF"); - testPaiement.setMethodePaiement("ESPECES"); - testPaiement.setStatutPaiement("EN_ATTENTE"); - testPaiement.setMembre(testMembre); - testPaiement.setActif(true); - - testCompte = new CompteEpargne(); - testCompte.setId(UUID.randomUUID()); - testCompte.setMembre(testMembre); - testCompte.setOrganisation(testOrganisation); - testCompte.setActif(true); - - // --- Mocks par défaut --- - when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com"); - when(typeReferenceRepository.findByDomaineAndCode(anyString(), anyString())) - .thenReturn(Optional.empty()); - when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("http://localhost:8085"); - - // membreRepository par défaut - when(membreRepository.findByEmail(TEST_USER_EMAIL)).thenReturn(Optional.of(testMembre)); - when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty()); - when(membreRepository.findByIdOptional(testMembre.getId())).thenReturn(Optional.of(testMembre)); - - // EntityManager mock (pour queries JPQL et find) - mockEm = mock(EntityManager.class); - when(paiementRepository.getEntityManager()).thenReturn(mockEm); - when(mockEm.merge(any())).thenAnswer(inv -> inv.getArgument(0)); - - TypedQuery mockPaiementQuery = mock(TypedQuery.class); - when(mockEm.createQuery(anyString(), eq(Paiement.class))).thenReturn(mockPaiementQuery); - when(mockPaiementQuery.setParameter(anyString(), any())).thenReturn(mockPaiementQuery); - when(mockPaiementQuery.setMaxResults(anyInt())).thenReturn(mockPaiementQuery); - when(mockPaiementQuery.getResultList()).thenReturn(List.of(testPaiement)); - - TypedQuery mockCotisationQuery = mock(TypedQuery.class); - when(mockEm.createQuery(anyString(), eq(Cotisation.class))).thenReturn(mockCotisationQuery); - when(mockCotisationQuery.setParameter(anyString(), any())).thenReturn(mockCotisationQuery); - when(mockCotisationQuery.getResultList()).thenReturn(List.of(testCotisation)); - - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - when(mockEm.find(eq(Cotisation.class), eq(autreCotisation.getId()))).thenReturn(autreCotisation); - when(mockEm.find(eq(Cotisation.class), any())).thenReturn(null); - - // persist mocks : set ID après persist - doAnswer(inv -> { - Object arg = inv.getArgument(0); - if (arg instanceof Paiement p && p.getId() == null) { - p.setId(UUID.randomUUID()); - } - return null; - }).when(paiementRepository).persist(any(Paiement.class)); - - doAnswer(inv -> { - Object arg = inv.getArgument(0); - if (arg instanceof IntentionPaiement ip && ip.getId() == null) { - ip.setId(UUID.randomUUID()); - } - return null; - }).when(intentionPaiementRepository).persist(any(IntentionPaiement.class)); - - // compteEpargne par défaut - when(compteEpargneRepository.findByIdOptional(testCompte.getId())) - .thenReturn(Optional.of(testCompte)); - - // paiementRepository : findPaiementById par défaut - when(paiementRepository.findPaiementById(testPaiement.getId())) - .thenReturn(Optional.of(testPaiement)); - when(paiementRepository.findPaiementById(any())) - .thenReturn(Optional.empty()); - when(paiementRepository.findPaiementById(testPaiement.getId())) - .thenReturn(Optional.of(testPaiement)); - - when(paiementRepository.findByNumeroReference("PAY-001")).thenReturn(Optional.of(testPaiement)); - when(paiementRepository.findByNumeroReference(anyString())).thenReturn(Optional.empty()); - when(paiementRepository.findByNumeroReference("PAY-001")).thenReturn(Optional.of(testPaiement)); - - when(paiementRepository.findByMembreId(testMembre.getId())).thenReturn(List.of(testPaiement)); - when(paiementRepository.findByMembreId(any())).thenReturn(List.of()); - when(paiementRepository.findByMembreId(testMembre.getId())).thenReturn(List.of(testPaiement)); - - when(paiementRepository.calculerMontantTotalValides(any(), any())) - .thenReturn(BigDecimal.valueOf(1000)); + cotisationRepository.persist(testCotisation); } - // ========================================================================= - // creerPaiement - // ========================================================================= + @AfterEach + @Transactional + void tearDown() { + // Supprimer Paiements du membre (Paiement n'a pas de lien direct cotisation, lien via PaiementObjet) + if (testMembre != null && testMembre.getId() != null) { + paiementRepository.getEntityManager() + .createQuery("DELETE FROM Paiement p WHERE p.membre.id = :membreId") + .setParameter("membreId", testMembre.getId()) + .executeUpdate(); + } + // Supprimer Cotisation + if (testCotisation != null && testCotisation.getId() != null) { + cotisationRepository.findByIdOptional(testCotisation.getId()) + .ifPresent(cotisationRepository::delete); + } + // Supprimer Membre + if (testMembre != null && testMembre.getId() != null) { + membreRepository.findByIdOptional(testMembre.getId()) + .ifPresent(membreRepository::delete); + } + // Supprimer Organisation + if (testOrganisation != null && testOrganisation.getId() != null) { + organisationRepository.findByIdOptional(testOrganisation.getId()) + .ifPresent(organisationRepository::delete); + } + } @Test - @DisplayName("creerPaiement avec membreId valide → crée le paiement avec le membre") - void creerPaiement_avecMembreId_creePaiement() { + @Order(1) + @TestTransaction + @DisplayName("creerPaiement avec données valides crée le paiement") + void creerPaiement_validRequest_createsPaiement() { String ref = "PAY-" + UUID.randomUUID().toString().substring(0, 8); CreatePaiementRequest request = CreatePaiementRequest.builder() .numeroReference(ref) @@ -248,502 +146,145 @@ class PaiementServiceTest { PaiementResponse response = paiementService.creerPaiement(request); assertThat(response).isNotNull(); - assertThat(response.getId()).isNotNull(); assertThat(response.getNumeroReference()).isEqualTo(ref); assertThat(response.getStatutPaiement()).isEqualTo("EN_ATTENTE"); - assertThat(response.getMontant()).isEqualByComparingTo(new BigDecimal("250.00")); - assertThat(response.getMembreId()).isEqualTo(testMembre.getId()); } @Test - @DisplayName("creerPaiement sans membreId → crée le paiement sans membre") - void creerPaiement_sansMembreId_creePaiementSansMembre() { - String ref = "PAY-NOMEMBRE-" + UUID.randomUUID().toString().substring(0, 6); + @Order(2) + @TestTransaction + @DisplayName("validerPaiement change le statut en VALIDE") + void validerPaiement_updatesStatus() { CreatePaiementRequest request = CreatePaiementRequest.builder() - .numeroReference(ref) - .montant(new BigDecimal("100.00")) + .numeroReference("REF-VAL-" + UUID.randomUUID().toString().substring(0, 5)) + .montant(BigDecimal.TEN) .codeDevise("EUR") .methodePaiement("VIREMENT") - .membreId(null) + .membreId(testMembre.getId()) .build(); + PaiementResponse created = paiementService.creerPaiement(request); - PaiementResponse response = paiementService.creerPaiement(request); + PaiementResponse validated = paiementService.validerPaiement(created.getId()); - assertThat(response).isNotNull(); - assertThat(response.getMembreId()).isNull(); + assertThat(validated.getStatutPaiement()).isEqualTo("VALIDE"); + assertThat(validated.getDateValidation()).isNotNull(); } @Test - @DisplayName("creerPaiement avec membreId introuvable → NotFoundException") - void creerPaiement_membreInexistant_throwsNotFoundException() { - UUID unknownId = UUID.randomUUID(); - when(membreRepository.findByIdOptional(unknownId)).thenReturn(Optional.empty()); - + @Order(3) + @TestTransaction + @DisplayName("annulerPaiement change le statut en ANNULE") + void annulerPaiement_updatesStatus() { CreatePaiementRequest request = CreatePaiementRequest.builder() - .numeroReference("PAY-UNKNOWN") - .montant(BigDecimal.TEN) - .codeDevise("XOF") - .methodePaiement("ESPECES") - .membreId(unknownId) - .build(); - - assertThatThrownBy(() -> paiementService.creerPaiement(request)) - .isInstanceOf(NotFoundException.class) - .hasMessageContaining("Membre non trouvé"); - } - - @Test - @DisplayName("creerPaiement avec transactionWave non null → répond avec transactionWaveId") - void creerPaiement_avecTransactionWave_repond() { - // Couvre la branche "if (paiement.getTransactionWave() != null)" dans convertToResponse - // En mode mock, transactionWave reste null → on couvre la branche null ici - CreatePaiementRequest request = CreatePaiementRequest.builder() - .numeroReference("PAY-TW-001") + .numeroReference("REF-ANN-" + UUID.randomUUID().toString().substring(0, 5)) .montant(BigDecimal.ONE) - .codeDevise("XOF") - .methodePaiement("ESPECES") - .membreId(null) + .codeDevise("USD") + .methodePaiement("CARTE") + .membreId(testMembre.getId()) .build(); + PaiementResponse created = paiementService.creerPaiement(request); - PaiementResponse response = paiementService.creerPaiement(request); + PaiementResponse cancelled = paiementService.annulerPaiement(created.getId()); - assertThat(response.getTransactionWaveId()).isNull(); // branche null de transactionWave - } - - // ========================================================================= - // validerPaiement - // ========================================================================= - - @Test - @DisplayName("validerPaiement sur paiement EN_ATTENTE → statut VALIDE avec dateValidation") - void validerPaiement_enAttente_retourneValide() { - Paiement paiement = new Paiement(); - paiement.setId(UUID.randomUUID()); - paiement.setNumeroReference("PAY-VAL-001"); - paiement.setMontant(BigDecimal.TEN); - paiement.setCodeDevise("EUR"); - paiement.setMethodePaiement("VIREMENT"); - paiement.setStatutPaiement("EN_ATTENTE"); - paiement.setMembre(testMembre); - when(paiementRepository.findPaiementById(paiement.getId())).thenReturn(Optional.of(paiement)); - - PaiementResponse response = paiementService.validerPaiement(paiement.getId()); - - assertThat(response.getStatutPaiement()).isEqualTo("VALIDE"); - assertThat(response.getDateValidation()).isNotNull(); + assertThat(cancelled.getStatutPaiement()).isEqualTo("ANNULE"); } @Test - @DisplayName("validerPaiement sur paiement déjà VALIDE → retourne sans modification") - void validerPaiement_dejaValide_retourneSansModification() { - Paiement paiement = new Paiement(); - paiement.setId(UUID.randomUUID()); - paiement.setNumeroReference("PAY-DEJA-VAL"); - paiement.setMontant(BigDecimal.ONE); - paiement.setCodeDevise("XOF"); - paiement.setMethodePaiement("ESPECES"); - paiement.setStatutPaiement("VALIDE"); - paiement.setMembre(testMembre); - when(paiementRepository.findPaiementById(paiement.getId())).thenReturn(Optional.of(paiement)); - - PaiementResponse response = paiementService.validerPaiement(paiement.getId()); - - assertThat(response.getStatutPaiement()).isEqualTo("VALIDE"); - } - - @Test - @DisplayName("validerPaiement avec ID inconnu → NotFoundException") - void validerPaiement_idInconnu_throwsNotFoundException() { - UUID unknownId = UUID.randomUUID(); - when(paiementRepository.findPaiementById(unknownId)).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> paiementService.validerPaiement(unknownId)) - .isInstanceOf(NotFoundException.class) - .hasMessageContaining("Paiement non trouvé"); - } - - // ========================================================================= - // annulerPaiement - // ========================================================================= - - @Test - @DisplayName("annulerPaiement sur paiement EN_ATTENTE → statut ANNULE") - void annulerPaiement_enAttente_retourneAnnule() { - Paiement paiement = new Paiement(); - paiement.setId(UUID.randomUUID()); - paiement.setNumeroReference("PAY-ANN-001"); - paiement.setMontant(BigDecimal.ONE); - paiement.setCodeDevise("USD"); - paiement.setMethodePaiement("CARTE"); - paiement.setStatutPaiement("EN_ATTENTE"); - paiement.setMembre(testMembre); - when(paiementRepository.findPaiementById(paiement.getId())).thenReturn(Optional.of(paiement)); - - PaiementResponse response = paiementService.annulerPaiement(paiement.getId()); - - assertThat(response.getStatutPaiement()).isEqualTo("ANNULE"); - } - - @Test - @DisplayName("annulerPaiement sur paiement VALIDE → IllegalStateException") - void annulerPaiement_dejaValide_throwsIllegalState() { - Paiement paiement = new Paiement(); - paiement.setId(UUID.randomUUID()); - paiement.setNumeroReference("PAY-ANN-VALIDE"); - paiement.setMontant(BigDecimal.TEN); - paiement.setCodeDevise("XOF"); - paiement.setMethodePaiement("ESPECES"); - paiement.setStatutPaiement("VALIDE"); - paiement.setMembre(testMembre); - when(paiementRepository.findPaiementById(paiement.getId())).thenReturn(Optional.of(paiement)); - - assertThatThrownBy(() -> paiementService.annulerPaiement(paiement.getId())) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("ne peut plus être annulé"); - } - - @Test - @DisplayName("annulerPaiement avec ID inconnu → NotFoundException") - void annulerPaiement_idInconnu_throwsNotFoundException() { - UUID unknownId = UUID.randomUUID(); - when(paiementRepository.findPaiementById(unknownId)).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> paiementService.annulerPaiement(unknownId)) - .isInstanceOf(NotFoundException.class) - .hasMessageContaining("Paiement non trouvé"); - } - - // ========================================================================= - // trouverParId - // ========================================================================= - - @Test - @DisplayName("trouverParId avec paiement existant → retourne le paiement") - void trouverParId_trouve_retournePaiement() { - PaiementResponse response = paiementService.trouverParId(testPaiement.getId()); - - assertThat(response).isNotNull(); - assertThat(response.getId()).isEqualTo(testPaiement.getId()); - } - - @Test - @DisplayName("trouverParId avec ID inconnu → NotFoundException") - void trouverParId_idInconnu_throwsNotFoundException() { - UUID unknownId = UUID.randomUUID(); - when(paiementRepository.findPaiementById(unknownId)).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> paiementService.trouverParId(unknownId)) - .isInstanceOf(NotFoundException.class) - .hasMessageContaining("Paiement non trouvé"); - } - - // ========================================================================= - // trouverParNumeroReference - // ========================================================================= - - @Test - @DisplayName("trouverParNumeroReference avec référence existante → retourne le paiement") - void trouverParNumeroReference_refTrouvee_retournePaiement() { - PaiementResponse response = paiementService.trouverParNumeroReference("PAY-001"); - - assertThat(response).isNotNull(); - assertThat(response.getNumeroReference()).isEqualTo("PAY-001"); - } - - @Test - @DisplayName("trouverParNumeroReference avec référence inconnue → NotFoundException") - void trouverParNumeroReference_refInconnue_throwsNotFoundException() { - assertThatThrownBy(() -> paiementService.trouverParNumeroReference("REF-INCONNU-99999")) - .isInstanceOf(NotFoundException.class) - .hasMessageContaining("Paiement non trouvé avec la référence"); - } - - // ========================================================================= - // listerParMembre - // ========================================================================= - - @Test - @DisplayName("listerParMembre avec paiements → retourne la liste") - void listerParMembre_avecPaiements_retourneListe() { - List results = paiementService.listerParMembre(testMembre.getId()); - - assertThat(results).isNotNull().isNotEmpty(); - } - - @Test - @DisplayName("listerParMembre avec liste contenant null → convertToSummaryResponse null branch couverte") - void listerParMembre_avecElementNull_couvreNullBranch() { - when(paiementRepository.findByMembreId(testMembre.getId())) - .thenReturn(Arrays.asList(null, testPaiement)); - - List results = paiementService.listerParMembre(testMembre.getId()); - - assertThat(results).hasSize(2); - assertThat(results.get(0)).isNull(); // convertToSummaryResponse(null) → null - } - - @Test - @DisplayName("listerParMembre sans paiements → retourne liste vide") - void listerParMembre_sansPaiements_retourneVide() { - List results = paiementService.listerParMembre(UUID.randomUUID()); - - assertThat(results).isNotNull().isEmpty(); - } - - // ========================================================================= - // calculerMontantTotalValides - // ========================================================================= - - @Test - @DisplayName("calculerMontantTotalValides → retourne la somme calculée") - void calculerMontantTotalValides_retourneSomme() { - BigDecimal total = paiementService.calculerMontantTotalValides( - LocalDateTime.now().minusYears(1), LocalDateTime.now().plusDays(1)); - - assertThat(total).isNotNull().isEqualByComparingTo(BigDecimal.valueOf(1000)); - } - - // ========================================================================= - // getMonHistoriquePaiements - // ========================================================================= - - @Test + @Order(4) + @TestTransaction @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("getMonHistoriquePaiements → retourne les paiements VALIDE du membre connecté") - void getMonHistoriquePaiements_membreConnecte_retournePaiements() { - Paiement paiementValide = new Paiement(); - paiementValide.setId(UUID.randomUUID()); - paiementValide.setNumeroReference("PAY-HIST-001"); - paiementValide.setMontant(BigDecimal.valueOf(5000)); - paiementValide.setCodeDevise("XOF"); - paiementValide.setMethodePaiement("ESPECES"); - paiementValide.setStatutPaiement("VALIDE"); - paiementValide.setDatePaiement(LocalDateTime.now()); - paiementValide.setMembre(testMembre); - - TypedQuery paiementQuery = mock(TypedQuery.class); - when(mockEm.createQuery(anyString(), eq(Paiement.class))).thenReturn(paiementQuery); - when(paiementQuery.setParameter(anyString(), any())).thenReturn(paiementQuery); - when(paiementQuery.setMaxResults(anyInt())).thenReturn(paiementQuery); - when(paiementQuery.getResultList()).thenReturn(List.of(paiementValide)); + @DisplayName("getMonHistoriquePaiements → retourne paiements validés du membre connecté") + @Transactional + void getMonHistoriquePaiements_returnsOnlyMemberValidatedPaiements() { + // Créer un paiement validé + Paiement paiement = new Paiement(); + paiement.setNumeroReference("PAY-HIST-" + UUID.randomUUID().toString().substring(0, 8)); + paiement.setMontant(BigDecimal.valueOf(5000)); + paiement.setCodeDevise("XOF"); + paiement.setMethodePaiement("ESPECES"); + paiement.setStatutPaiement("VALIDE"); + paiement.setDatePaiement(LocalDateTime.now()); + paiement.setDateValidation(LocalDateTime.now()); + paiement.setMembre(testMembre); + paiement.setDateCreation(LocalDateTime.now()); + paiement.setActif(true); + paiementRepository.persist(paiement); List results = paiementService.getMonHistoriquePaiements(5); - assertThat(results).isNotNull().isNotEmpty(); - assertThat(results.get(0).getStatutPaiement()).isEqualTo("VALIDE"); + assertThat(results).isNotNull(); + assertThat(results).isNotEmpty(); + assertThat(results).allMatch(p -> p.statutPaiement().equals("VALIDE")); + assertThat(results.get(0).id()).isEqualTo(paiement.getId()); } @Test - @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"}) - @DisplayName("getMonHistoriquePaiements — membre connecté introuvable → NotFoundException") - void getMonHistoriquePaiements_membreNonTrouve_throwsNotFoundException() { - when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty()); + @Order(5) + @TestTransaction + @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) + @DisplayName("getMonHistoriquePaiements → respecte la limite") + @Transactional + void getMonHistoriquePaiements_respectsLimit() { + // Créer 3 paiements validés + for (int i = 0; i < 3; i++) { + Paiement paiement = new Paiement(); + paiement.setNumeroReference("PAY-LIMIT-" + i + "-" + System.currentTimeMillis()); + paiement.setMontant(BigDecimal.valueOf(1000)); + paiement.setCodeDevise("XOF"); + paiement.setMethodePaiement("ESPECES"); + paiement.setStatutPaiement("VALIDE"); + paiement.setDatePaiement(LocalDateTime.now().minusDays(i)); + paiement.setDateValidation(LocalDateTime.now().minusDays(i)); + paiement.setMembre(testMembre); + paiement.setDateCreation(LocalDateTime.now()); + paiement.setActif(true); + paiementRepository.persist(paiement); + } + List results = paiementService.getMonHistoriquePaiements(2); + + assertThat(results).isNotNull(); + assertThat(results).hasSize(2); + } + + @Test + @Order(6) + @TestTransaction + @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"}) + @DisplayName("getMonHistoriquePaiements → membre non trouvé → NotFoundException") + void getMonHistoriquePaiements_membreNonTrouve_throws() { assertThatThrownBy(() -> paiementService.getMonHistoriquePaiements(5)) .isInstanceOf(NotFoundException.class) .hasMessageContaining("Membre non trouvé"); } - // ========================================================================= - // initierPaiementEnLigne — Wave happy path - // ========================================================================= - @Test + @Order(7) + @TestTransaction @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne WAVE → crée session Wave et retourne wave_launch_url") - void initierPaiementEnLigne_wave_success() throws WaveCheckoutException { - WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("wave-session-abc123", "https://wave.com/launch/abc123"); - when(waveCheckoutService.createSession(anyString(), anyString(), anyString(), - anyString(), anyString(), anyString())).thenReturn(session); - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("WAVE") - .numeroTelephone("771234567") - .build(); + @DisplayName("initierPaiementEnLigne → crée paiement avec statut EN_ATTENTE") + void initierPaiementEnLigne_createsPaiement() { + InitierPaiementEnLigneRequest request = new InitierPaiementEnLigneRequest( + testCotisation.getId(), "WAVE", "771234567"); PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request); assertThat(response).isNotNull(); assertThat(response.getTransactionId()).isNotNull(); - assertThat(response.getRedirectUrl()).isEqualTo("https://wave.com/launch/abc123"); + assertThat(response.getRedirectUrl()).isNotNull(); assertThat(response.getStatut()).isEqualTo("EN_ATTENTE"); assertThat(response.getMethodePaiement()).isEqualTo("WAVE"); + assertThat(response.getMontant()).isEqualByComparingTo(BigDecimal.valueOf(5000)); } @Test + @Order(8) + @TestTransaction @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne WAVE — cotisation sans codeDevise → default XOF (couvre L320)") - void initierPaiementEnLigne_wave_cotisationSansCodeDevise_defaultXof() throws WaveCheckoutException { - WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("wave-s-nodevise", "https://wave.com/nodevise"); - when(waveCheckoutService.createSession(anyString(), anyString(), anyString(), - anyString(), anyString(), anyString())).thenReturn(session); - Cotisation cotisationSansDevise = new Cotisation(); - cotisationSansDevise.setId(UUID.randomUUID()); - cotisationSansDevise.setMembre(testMembre); - cotisationSansDevise.setOrganisation(testOrganisation); - cotisationSansDevise.setMontantDu(BigDecimal.valueOf(3000)); - cotisationSansDevise.setCodeDevise(null); // branche null → "XOF" - cotisationSansDevise.setNumeroReference("COT-NODEVISE"); - cotisationSansDevise.setStatut("EN_ATTENTE"); - cotisationSansDevise.setActif(true); - when(mockEm.find(eq(Cotisation.class), eq(cotisationSansDevise.getId()))).thenReturn(cotisationSansDevise); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(cotisationSansDevise.getId()) - .methodePaiement("WAVE") - .numeroTelephone("771234567") - .build(); - - PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request); - - assertThat(response).isNotNull(); - assertThat(response.getStatut()).isEqualTo("EN_ATTENTE"); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne WAVE — numéro commençant par 0 (branche toE164)") - void initierPaiementEnLigne_wave_numeroCommencantPar0() throws WaveCheckoutException { - WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("wave-s-0", "https://wave.com/0"); - when(waveCheckoutService.createSession(anyString(), anyString(), anyString(), - anyString(), anyString(), anyString())).thenReturn(session); - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("WAVE") - .numeroTelephone("077123456") // 9 chiffres, commence par 0 - .build(); - - PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request); - - assertThat(response).isNotNull(); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne WAVE — numéro avec indicatif 225 (branche toE164)") - void initierPaiementEnLigne_wave_numeroAvecIndicatif225() throws WaveCheckoutException { - WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("wave-s-225", "https://wave.com/225"); - when(waveCheckoutService.createSession(anyString(), anyString(), anyString(), - anyString(), anyString(), anyString())).thenReturn(session); - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("WAVE") - .numeroTelephone("225771234567") // 12 chiffres, commence par 225 - .build(); - - PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request); - - assertThat(response).isNotNull(); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne WAVE — erreur Wave → BadRequestException") - void initierPaiementEnLigne_wave_erreur_throwsBadRequest() throws WaveCheckoutException { - doThrow(new WaveCheckoutException("API error")) - .when(waveCheckoutService).createSession(anyString(), anyString(), anyString(), - anyString(), anyString(), anyString()); - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("WAVE") - .numeroTelephone("771234567") - .build(); - - assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request)) - .isInstanceOf(jakarta.ws.rs.BadRequestException.class); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne ORANGE_MONEY → retourne URL orange-money") - void initierPaiementEnLigne_orangeMoney_success() { - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("ORANGE_MONEY") - .numeroTelephone("771234567") - .build(); - - PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request); - - assertThat(response).isNotNull(); - assertThat(response.getRedirectUrl()).contains("orange-money.com"); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne FREE_MONEY → retourne URL free-money") - void initierPaiementEnLigne_freeMoney_success() { - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("FREE_MONEY") - .numeroTelephone("771234567") - .build(); - - PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request); - - assertThat(response).isNotNull(); - assertThat(response.getRedirectUrl()).contains("free-money.com"); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne CARTE_BANCAIRE → retourne URL payment-gateway") - void initierPaiementEnLigne_carteBancaire_success() { - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("CARTE_BANCAIRE") - .numeroTelephone("771234567") - .build(); - - PaiementGatewayResponse response = paiementService.initierPaiementEnLigne(request); - - assertThat(response).isNotNull(); - assertThat(response.getRedirectUrl()).contains("payment-gateway.com"); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne méthode inconnue → IllegalArgumentException") - void initierPaiementEnLigne_methodeInconnue_throwsIllegalArgument() { - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("CHEQUE") - .numeroTelephone("771234567") - .build(); - - assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("non supportée"); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne — cotisation introuvable → NotFoundException") - void initierPaiementEnLigne_cotisationInexistante_throwsNotFoundException() { - UUID unknownId = UUID.randomUUID(); - when(mockEm.find(eq(Cotisation.class), eq(unknownId))).thenReturn(null); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(unknownId) - .methodePaiement("WAVE") - .numeroTelephone("771234567") - .build(); + @DisplayName("initierPaiementEnLigne → cotisation inexistante → NotFoundException") + void initierPaiementEnLigne_cotisationInexistante_throws() { + InitierPaiementEnLigneRequest request = new InitierPaiementEnLigneRequest( + UUID.randomUUID(), "WAVE", "771234567"); assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request)) .isInstanceOf(NotFoundException.class) @@ -751,159 +292,62 @@ class PaiementServiceTest { } @Test + @Order(9) + @TestTransaction @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne — cotisation appartient à un autre membre → IllegalArgumentException") - void initierPaiementEnLigne_cotisationAutreMembre_throwsIllegalArgument() { - when(mockEm.find(eq(Cotisation.class), eq(autreCotisation.getId()))).thenReturn(autreCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(autreCotisation.getId()) - .methodePaiement("WAVE") - .numeroTelephone("771234567") + @DisplayName("initierPaiementEnLigne → cotisation n'appartient pas au membre → IllegalArgumentException") + @Transactional + void initierPaiementEnLigne_cotisationNonAutorisee_throws() { + // Créer un autre membre (numeroMembre max 20 caractères en base) + Membre autreMembre = Membre.builder() + .numeroMembre("M-A-" + UUID.randomUUID().toString().substring(0, 6)) + .nom("Autre") + .prenom("Membre") + .email("autre-membre-" + System.currentTimeMillis() + "@test.com") + .dateNaissance(LocalDate.of(1985, 5, 5)) .build(); + autreMembre.setDateCreation(LocalDateTime.now()); + autreMembre.setActif(true); + membreRepository.persist(autreMembre); + + // Créer une cotisation pour l'autre membre + Cotisation autreCotisation = Cotisation.builder() + .typeCotisation("MENSUELLE") + .libelle("Cotisation autre membre") + .montantDu(BigDecimal.valueOf(3000)) + .montantPaye(BigDecimal.ZERO) + .codeDevise("XOF") + .statut("EN_ATTENTE") + .dateEcheance(LocalDate.now().plusMonths(1)) + .annee(LocalDate.now().getYear()) + .membre(autreMembre) + .organisation(testOrganisation) + .build(); + autreCotisation.setNumeroReference(Cotisation.genererNumeroReference()); + autreCotisation.setDateCreation(LocalDateTime.now()); + autreCotisation.setActif(true); + cotisationRepository.persist(autreCotisation); + + InitierPaiementEnLigneRequest request = new InitierPaiementEnLigneRequest( + autreCotisation.getId(), "WAVE", "771234567"); assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("n'appartient pas au membre connecté"); + + // Cleanup + cotisationRepository.delete(autreCotisation); + membreRepository.delete(autreMembre); } @Test - @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"}) - @DisplayName("initierPaiementEnLigne — membre connecté introuvable → NotFoundException") - void initierPaiementEnLigne_membreNonTrouve_throwsNotFoundException() { - when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty()); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("WAVE") - .numeroTelephone("771234567") - .build(); - - assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request)) - .isInstanceOf(NotFoundException.class) - .hasMessageContaining("Membre non trouvé"); - } - - // ========================================================================= - // initierDepotEpargneEnLigne - // ========================================================================= - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierDepotEpargneEnLigne WAVE → crée intention et retourne wave_launch_url") - void initierDepotEpargneEnLigne_wave_success() throws WaveCheckoutException { - WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("epargne-session-xyz", "https://wave.com/epargne/xyz"); - when(waveCheckoutService.createSession(anyString(), anyString(), anyString(), - anyString(), anyString(), anyString())).thenReturn(session); - - InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder() - .compteId(testCompte.getId()) - .montant(new BigDecimal("10000")) - .numeroTelephone("771234567") - .build(); - - PaiementGatewayResponse response = paiementService.initierDepotEpargneEnLigne(request); - - assertThat(response).isNotNull(); - assertThat(response.getRedirectUrl()).isEqualTo("https://wave.com/epargne/xyz"); - assertThat(response.getStatut()).isEqualTo("EN_ATTENTE"); - assertThat(response.getMethodePaiement()).isEqualTo("WAVE"); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierDepotEpargneEnLigne — erreur Wave → BadRequestException + intention ECHOUEE") - void initierDepotEpargneEnLigne_wave_erreur_throwsBadRequest() throws WaveCheckoutException { - doThrow(new WaveCheckoutException("Wave down")) - .when(waveCheckoutService).createSession(anyString(), anyString(), anyString(), - anyString(), anyString(), anyString()); - - InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder() - .compteId(testCompte.getId()) - .montant(new BigDecimal("5000")) - .numeroTelephone("771234567") - .build(); - - assertThatThrownBy(() -> paiementService.initierDepotEpargneEnLigne(request)) - .isInstanceOf(jakarta.ws.rs.BadRequestException.class); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierDepotEpargneEnLigne — compte introuvable → NotFoundException") - void initierDepotEpargneEnLigne_compteInexistant_throwsNotFoundException() { - UUID unknownId = UUID.randomUUID(); - when(compteEpargneRepository.findByIdOptional(unknownId)).thenReturn(Optional.empty()); - - InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder() - .compteId(unknownId) - .montant(new BigDecimal("5000")) - .numeroTelephone("771234567") - .build(); - - assertThatThrownBy(() -> paiementService.initierDepotEpargneEnLigne(request)) - .isInstanceOf(NotFoundException.class) - .hasMessageContaining("Compte épargne non trouvé"); - } - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierDepotEpargneEnLigne — compte appartient à un autre membre → IllegalArgumentException") - void initierDepotEpargneEnLigne_compteAutreMembre_throwsIllegalArgument() { - CompteEpargne autreCompte = new CompteEpargne(); - autreCompte.setId(UUID.randomUUID()); - autreCompte.setMembre(autreMembre); - autreCompte.setOrganisation(testOrganisation); - when(compteEpargneRepository.findByIdOptional(autreCompte.getId())) - .thenReturn(Optional.of(autreCompte)); - - InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder() - .compteId(autreCompte.getId()) - .montant(new BigDecimal("5000")) - .numeroTelephone("771234567") - .build(); - - assertThatThrownBy(() -> paiementService.initierDepotEpargneEnLigne(request)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("n'appartient pas au membre connecté"); - } - - @Test - @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"}) - @DisplayName("initierDepotEpargneEnLigne — membre connecté introuvable → NotFoundException") - void initierDepotEpargneEnLigne_membreNonTrouve_throwsNotFoundException() { - when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty()); - - InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder() - .compteId(testCompte.getId()) - .montant(new BigDecimal("5000")) - .numeroTelephone("771234567") - .build(); - - assertThatThrownBy(() -> paiementService.initierDepotEpargneEnLigne(request)) - .isInstanceOf(NotFoundException.class) - .hasMessageContaining("Membre non trouvé"); - } - - // ========================================================================= - // declarerPaiementManuel - // ========================================================================= - - @Test + @Order(10) + @TestTransaction @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) @DisplayName("declarerPaiementManuel → crée paiement avec statut EN_ATTENTE_VALIDATION") - void declarerPaiementManuel_success() { - TypedQuery cotisationQuery = mock(TypedQuery.class); - when(mockEm.createQuery(anyString(), eq(Cotisation.class))).thenReturn(cotisationQuery); - when(cotisationQuery.setParameter(anyString(), any())).thenReturn(cotisationQuery); - when(cotisationQuery.getResultList()).thenReturn(List.of(testCotisation)); - - DeclarerPaiementManuelRequest request = DeclarerPaiementManuelRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("ESPECES") - .reference("REF-MANUEL-001") - .commentaire("Paiement au trésorier") - .build(); + void declarerPaiementManuel_createsPaiement() { + DeclarerPaiementManuelRequest request = new DeclarerPaiementManuelRequest( + testCotisation.getId(), "ESPECES", "REF-MANUEL-001", "Paiement effectué au trésorier"); PaiementResponse response = paiementService.declarerPaiementManuel(request); @@ -912,24 +356,17 @@ class PaiementServiceTest { assertThat(response.getStatutPaiement()).isEqualTo("EN_ATTENTE_VALIDATION"); assertThat(response.getMethodePaiement()).isEqualTo("ESPECES"); assertThat(response.getReferenceExterne()).isEqualTo("REF-MANUEL-001"); - assertThat(response.getCommentaire()).isEqualTo("Paiement au trésorier"); + assertThat(response.getCommentaire()).isEqualTo("Paiement effectué au trésorier"); } @Test + @Order(11) + @TestTransaction @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("declarerPaiementManuel — cotisation introuvable → NotFoundException") - void declarerPaiementManuel_cotisationInexistante_throwsNotFoundException() { - TypedQuery cotisationQuery = mock(TypedQuery.class); - when(mockEm.createQuery(anyString(), eq(Cotisation.class))).thenReturn(cotisationQuery); - when(cotisationQuery.setParameter(anyString(), any())).thenReturn(cotisationQuery); - when(cotisationQuery.getResultList()).thenReturn(List.of()); - - DeclarerPaiementManuelRequest request = DeclarerPaiementManuelRequest.builder() - .cotisationId(UUID.randomUUID()) - .methodePaiement("ESPECES") - .reference("REF-001") - .commentaire("Test") - .build(); + @DisplayName("declarerPaiementManuel → cotisation inexistante → NotFoundException") + void declarerPaiementManuel_cotisationInexistante_throws() { + DeclarerPaiementManuelRequest request = new DeclarerPaiementManuelRequest( + UUID.randomUUID(), "ESPECES", "REF-001", "Test"); assertThatThrownBy(() -> paiementService.declarerPaiementManuel(request)) .isInstanceOf(NotFoundException.class) @@ -937,305 +374,79 @@ class PaiementServiceTest { } @Test + @Order(12) + @TestTransaction @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("declarerPaiementManuel — cotisation appartient à un autre membre → IllegalArgumentException") - void declarerPaiementManuel_cotisationAutreMembre_throwsIllegalArgument() { - TypedQuery cotisationQuery = mock(TypedQuery.class); - when(mockEm.createQuery(anyString(), eq(Cotisation.class))).thenReturn(cotisationQuery); - when(cotisationQuery.setParameter(anyString(), any())).thenReturn(cotisationQuery); - when(cotisationQuery.getResultList()).thenReturn(List.of(autreCotisation)); - - DeclarerPaiementManuelRequest request = DeclarerPaiementManuelRequest.builder() - .cotisationId(autreCotisation.getId()) - .methodePaiement("ESPECES") - .reference("REF-001") - .commentaire("Test") + @DisplayName("declarerPaiementManuel → cotisation n'appartient pas au membre → IllegalArgumentException") + @Transactional + void declarerPaiementManuel_cotisationNonAutorisee_throws() { + // Créer un autre membre (numeroMembre max 20 caractères en base) + Membre autreMembre = Membre.builder() + .numeroMembre("M-A2-" + UUID.randomUUID().toString().substring(0, 6)) + .nom("Autre") + .prenom("Membre") + .email("autre-membre2-" + System.currentTimeMillis() + "@test.com") + .dateNaissance(LocalDate.of(1985, 5, 5)) .build(); + autreMembre.setDateCreation(LocalDateTime.now()); + autreMembre.setActif(true); + membreRepository.persist(autreMembre); + + // Créer une cotisation pour l'autre membre + Cotisation autreCotisation = Cotisation.builder() + .typeCotisation("MENSUELLE") + .libelle("Cotisation autre membre") + .montantDu(BigDecimal.valueOf(3000)) + .montantPaye(BigDecimal.ZERO) + .codeDevise("XOF") + .statut("EN_ATTENTE") + .dateEcheance(LocalDate.now().plusMonths(1)) + .annee(LocalDate.now().getYear()) + .membre(autreMembre) + .organisation(testOrganisation) + .build(); + autreCotisation.setNumeroReference(Cotisation.genererNumeroReference()); + autreCotisation.setDateCreation(LocalDateTime.now()); + autreCotisation.setActif(true); + cotisationRepository.persist(autreCotisation); + + DeclarerPaiementManuelRequest request = new DeclarerPaiementManuelRequest( + autreCotisation.getId(), "ESPECES", "REF-001", "Test"); assertThatThrownBy(() -> paiementService.declarerPaiementManuel(request)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("n'appartient pas au membre connecté"); + + // Cleanup + cotisationRepository.delete(autreCotisation); + membreRepository.delete(autreMembre); } @Test + @Order(13) + @TestTransaction @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"}) - @DisplayName("declarerPaiementManuel — membre connecté introuvable → NotFoundException") - void declarerPaiementManuel_membreNonTrouve_throwsNotFoundException() { - when(membreRepository.findByEmail("membre-inexistant@test.com")).thenReturn(Optional.empty()); + @DisplayName("initierPaiementEnLigne → membre non trouvé → NotFoundException") + void initierPaiementEnLigne_membreNonTrouve_throws() { + InitierPaiementEnLigneRequest request = new InitierPaiementEnLigneRequest( + testCotisation.getId(), "WAVE", "771234567"); - DeclarerPaiementManuelRequest request = DeclarerPaiementManuelRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("ESPECES") - .reference("REF-001") - .commentaire("Test") - .build(); + assertThatThrownBy(() -> paiementService.initierPaiementEnLigne(request)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Membre non trouvé"); + } + + @Test + @Order(14) + @TestTransaction + @TestSecurity(user = "membre-inexistant@test.com", roles = {"MEMBRE"}) + @DisplayName("declarerPaiementManuel → membre non trouvé → NotFoundException") + void declarerPaiementManuel_membreNonTrouve_throws() { + DeclarerPaiementManuelRequest request = new DeclarerPaiementManuelRequest( + testCotisation.getId(), "ESPECES", "REF-001", "Test"); assertThatThrownBy(() -> paiementService.declarerPaiementManuel(request)) .isInstanceOf(NotFoundException.class) .hasMessageContaining("Membre non trouvé"); } - - // ========================================================================= - // toE164 — branches manquantes via réflexion - // ========================================================================= - - @Test - @DisplayName("toE164 null → retourne null") - void toE164_null_returnsNull() throws Exception { - java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class); - toE164.setAccessible(true); - Object result = toE164.invoke(null, (Object) null); - assertThat(result).isNull(); - } - - @Test - @DisplayName("toE164 blank string → retourne null") - void toE164_blank_returnsNull() throws Exception { - java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class); - toE164.setAccessible(true); - Object result = toE164.invoke(null, " "); - assertThat(result).isNull(); - } - - @Test - @DisplayName("toE164 numéro 9 chiffres commençant par 7 → +225 + digits") - void toE164_9DigitsStartingWith7_returnsWith225() throws Exception { - java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class); - toE164.setAccessible(true); - Object result = toE164.invoke(null, "771234567"); - assertThat(result).isEqualTo("+225771234567"); - } - - @Test - @DisplayName("toE164 numéro 9 chiffres commençant par 0 → +225 + digits[1:]") - void toE164_9DigitsStartingWith0_removesLeadingZero() throws Exception { - java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class); - toE164.setAccessible(true); - Object result = toE164.invoke(null, "077123456"); - assertThat(result).isEqualTo("+22577123456"); - } - - @Test - @DisplayName("toE164 numéro commençant par 225 (>=9 chiffres) → +digits") - void toE164_startsWithIndicatif225_returnsPlusDigits() throws Exception { - java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class); - toE164.setAccessible(true); - Object result = toE164.invoke(null, "225771234567"); - assertThat(result).isEqualTo("+225771234567"); - } - - @Test - @DisplayName("toE164 numéro déjà avec + → retourné tel quel") - void toE164_alreadyHasPlus_returnsAsIs() throws Exception { - java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class); - toE164.setAccessible(true); - Object result = toE164.invoke(null, "+33612345678"); - assertThat(result).isEqualTo("+33612345678"); - } - - @Test - @DisplayName("toE164 numéro autre format (pas 9 chiffres, pas 225, pas +) → +digits") - void toE164_otherFormat_returnsPlusDigits() throws Exception { - java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class); - toE164.setAccessible(true); - Object result = toE164.invoke(null, "3361234567"); - assertThat(result).isEqualTo("+3361234567"); - } - - // ========================================================================= - // convertToResponse — branche transactionWave non-null - // ========================================================================= - - @Test - @DisplayName("convertToResponse avec transactionWave non-null → set transactionWaveId") - void convertToResponse_avecTransactionWave_setsTransactionWaveId() { - UUID paiementId = UUID.randomUUID(); - dev.lions.unionflow.server.entity.TransactionWave wave = - new dev.lions.unionflow.server.entity.TransactionWave(); - wave.setId(UUID.randomUUID()); - - Paiement paiement = new Paiement(); - paiement.setId(paiementId); - paiement.setNumeroReference("PAY-TW-BRANCH"); - paiement.setMontant(BigDecimal.ONE); - paiement.setCodeDevise("XOF"); - paiement.setMethodePaiement("WAVE"); - paiement.setStatutPaiement("VALIDE"); - paiement.setTransactionWave(wave); // non-null → branche transactionWave != null - paiement.setActif(true); - - when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement)); - - dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse response = - paiementService.trouverParId(paiementId); - - assertThat(response).isNotNull(); - assertThat(response.getTransactionWaveId()).isEqualTo(wave.getId()); - } - - // ========================================================================= - // resolveLibelle — branche found (TypeReference trouvé) - // resolveSeverity — branche found (TypeReference trouvé) - // ========================================================================= - - @Test - @DisplayName("resolveLibelle retourne le libellé de la TypeReference quand trouvée") - void resolveLibelle_found_returnsLibelle() { - UUID paiementId = UUID.randomUUID(); - - dev.lions.unionflow.server.entity.TypeReference typeRef = - new dev.lions.unionflow.server.entity.TypeReference(); - typeRef.setLibelle("Espèces"); - typeRef.setSeverity("info"); - when(typeReferenceRepository.findByDomaineAndCode("METHODE_PAIEMENT", "ESPECES")) - .thenReturn(Optional.of(typeRef)); - - dev.lions.unionflow.server.entity.TypeReference statutRef = - new dev.lions.unionflow.server.entity.TypeReference(); - statutRef.setLibelle("Validé"); - statutRef.setSeverity("success"); - when(typeReferenceRepository.findByDomaineAndCode("STATUT_PAIEMENT", "VALIDE")) - .thenReturn(Optional.of(statutRef)); - - Paiement paiement = new Paiement(); - paiement.setId(paiementId); - paiement.setNumeroReference("PAY-LIBELLE"); - paiement.setMontant(BigDecimal.TEN); - paiement.setCodeDevise("XOF"); - paiement.setMethodePaiement("ESPECES"); - paiement.setStatutPaiement("VALIDE"); - paiement.setActif(true); - - when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement)); - - dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse response = - paiementService.trouverParId(paiementId); - - assertThat(response).isNotNull(); - assertThat(response.getMethodePaiementLibelle()).isEqualTo("Espèces"); - assertThat(response.getStatutPaiementLibelle()).isEqualTo("Validé"); - assertThat(response.getStatutPaiementSeverity()).isEqualTo("success"); - } - - // ========================================================================= - // initierPaiementWave — branche getRedirectBaseUrl avec trailing slash - // ========================================================================= - - @Test - @TestSecurity(user = TEST_USER_EMAIL, roles = {"MEMBRE"}) - @DisplayName("initierPaiementWave avec URL de base ayant un slash final → slash supprimé") - void initierPaiementWave_urlWithTrailingSlash_removesSlash() throws Exception { - // Configurer la base URL avec un slash final → replaceAll("/+$", "") sera exercé - when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("http://localhost:8085/"); - - WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse( - "session-slash-test", "https://wave.com/slash"); - when(waveCheckoutService.createSession(anyString(), anyString(), anyString(), - anyString(), anyString(), anyString())).thenReturn(session); - when(mockEm.find(eq(Cotisation.class), eq(testCotisation.getId()))).thenReturn(testCotisation); - - InitierPaiementEnLigneRequest request = InitierPaiementEnLigneRequest.builder() - .cotisationId(testCotisation.getId()) - .methodePaiement("WAVE") - .numeroTelephone("771234567") - .build(); - - dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse response = - paiementService.initierPaiementEnLigne(request); - - assertThat(response).isNotNull(); - assertThat(response.getRedirectUrl()).isEqualTo("https://wave.com/slash"); - } - - // ========================================================================= - // resolveSeverity — branche code null (retourne null immédiatement) - // ========================================================================= - - @Test - @DisplayName("convertToResponse avec membre=null — branche if(membre!=null) false couverte") - void convertToResponse_membreNull_membreIdNotSet() { - UUID paiementId = UUID.randomUUID(); - - Paiement paiement = new Paiement(); - paiement.setId(paiementId); - paiement.setNumeroReference("PAY-NO-MEMBRE"); - paiement.setMontant(BigDecimal.TEN); - paiement.setCodeDevise("XOF"); - paiement.setMembre(null); // branche if(membre != null) → false - paiement.setActif(true); - - when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement)); - - dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse response = - paiementService.trouverParId(paiementId); - - assertThat(response).isNotNull(); - assertThat(response.getMembreId()).isNull(); - } - - @Test - @DisplayName("resolveSeverity avec code null retourne null (via convertToSummaryResponse)") - void resolveSeverity_nullCode_returnsNull() throws Exception { - // paiement sans statutPaiement et sans methodePaiement → code null dans resolveLibelle/resolveSeverity - Paiement paiement = new Paiement(); - paiement.setId(UUID.randomUUID()); - paiement.setNumeroReference("PAY-NULL-CODE"); - paiement.setMontant(BigDecimal.ONE); - paiement.setCodeDevise("XOF"); - paiement.setMethodePaiement(null); // → resolveLibelle avec code null → return null - paiement.setStatutPaiement(null); // → resolveSeverity avec code null → return null - paiement.setActif(true); - - when(paiementRepository.findByMembreId(testMembre.getId())) - .thenReturn(List.of(paiement)); - - // listerParMembre appelle convertToSummaryResponse qui appelle resolveLibelle/resolveSeverity - List results = paiementService.listerParMembre(testMembre.getId()); - - assertThat(results).isNotNull(); - assertThat(results).isNotEmpty(); - } - - @Test - @DisplayName("toE164 numéro 9 chiffres ne commençant ni par 7 ni par 0 → +digits") - void toE164_9DigitsNotStartingWith7Or0_returnsPlusDigits() throws Exception { - java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class); - toE164.setAccessible(true); - // 9 chiffres commençant par 5 (ni 7 ni 0) → aucune règle de normalisation ne s'applique → préfixe + - Object result = toE164.invoke(null, "577123456"); - assertThat(result).isEqualTo("+577123456"); - } - - @Test - @DisplayName("toE164 numéro court (< 9 chiffres) → +digits") - void toE164_shortNumber_length8_skipsL395() throws Exception { - java.lang.reflect.Method toE164 = PaiementService.class.getDeclaredMethod("toE164", String.class); - toE164.setAccessible(true); - // 8 chiffres → aucune règle de normalisation ne s'applique → préfixe + - Object result = toE164.invoke(null, "12345678"); - assertThat(result).isEqualTo("+12345678"); - } - - @Test - @DisplayName("enrichirLibelles — methodePaiement null, statutPaiement non-null → libellé méthode absent") - void enrichirLibelles_methodePaiementNull_statutNonNull_covers608() { - UUID paiementId = UUID.randomUUID(); - - Paiement paiement = new Paiement(); - paiement.setId(paiementId); - paiement.setNumeroReference("PAY-608-BRANCH"); - paiement.setMontant(BigDecimal.TEN); - paiement.setCodeDevise("XOF"); - paiement.setMethodePaiement(null); - paiement.setStatutPaiement("EN_ATTENTE"); - paiement.setActif(true); - - when(paiementRepository.findPaiementById(paiementId)).thenReturn(Optional.of(paiement)); - - dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse response = - paiementService.trouverParId(paiementId); - - assertThat(response).isNotNull(); - assertThat(response.getMethodePaiementLibelle()).isNull(); - } }