fix(paiement): rendre colonnes legacy nullables + refactor Paiement/PaiementObjet
Migrations : - V25 : numero_transaction nullable dans paiements (legacy V1 NOT NULL bloquant INSERT) - V26 : autres colonnes legacy NOT NULL V1 (type_paiement, statut_paiement, etc.) rendues nullables pour alignement avec l'entité Paiement Refactor Paiement/PaiementObjet : mise à jour entités, repository, resource, service pour cohérence avec le nouveau module Versement. Tests associés supprimés/ajustés.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* 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)}.
|
||||
*
|
||||
* <p>
|
||||
* 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).
|
||||
* <p>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}).
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Paiement, UUID> {
|
||||
|
||||
/**
|
||||
* 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<Paiement> 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<Paiement> 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<Paiement> 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<Paiement> 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<Paiement> 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<Paiement> 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<Paiement> 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<Paiement> 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<Paiement> paiements = findValidesParPeriode(dateDebut, dateFin);
|
||||
return paiements.stream()
|
||||
return findValidesParPeriode(dateDebut, dateFin).stream()
|
||||
.map(Paiement::getMontant)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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).
|
||||
*
|
||||
* <p>Endpoints principaux :
|
||||
* <ul>
|
||||
* <li>{@code POST /api/paiements/initier-paiement-en-ligne} — démarre le flux Wave QR code</li>
|
||||
* <li>{@code GET /api/paiements/statut-intention/{intentionId}} — polling du statut Wave</li>
|
||||
* <li>{@code POST /api/paiements/declarer-manuel} — paiement manuel (espèces/virement)</li>
|
||||
* <li>{@code GET /api/paiements/mon-historique} — historique du membre connecté</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<PaiementSummaryResponse> 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<PaiementSummaryResponse> 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<PaiementSummaryResponse> 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<PaiementSummaryResponse> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<Paiement> opt = paiementRepository.findPaiementById(UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByNumeroReference retourne empty pour référence inexistante")
|
||||
void findByNumeroReference_inexistant_returnsEmpty() {
|
||||
Optional<Paiement> opt = paiementRepository.findByNumeroReference("REF-" + UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("listAll retourne une liste")
|
||||
void listAll_returnsList() {
|
||||
List<Paiement> 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<Paiement> 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<Paiement> 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<Paiement> 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<Paiement> list = paiementRepository.findByMembreId(UUID.randomUUID());
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
}
|
||||
@@ -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<PaiementSummaryResponse> 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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user