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 jakarta.validation.constraints.*;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@@ -15,8 +14,8 @@ import lombok.EqualsAndHashCode;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entité Paiement centralisée pour tous les types de paiements
|
* Entité Paiement centralisée pour tous les types de paiements.
|
||||||
* Réutilisable pour cotisations, adhésions, événements, aides
|
* Réutilisable pour cotisations, adhésions, événements, aides.
|
||||||
*
|
*
|
||||||
* @author UnionFlow Team
|
* @author UnionFlow Team
|
||||||
* @version 3.0
|
* @version 3.0
|
||||||
@@ -104,7 +103,7 @@ public class Paiement extends BaseEntity {
|
|||||||
@JoinColumn(name = "membre_id", nullable = false)
|
@JoinColumn(name = "membre_id", nullable = false)
|
||||||
private Membre membre;
|
private Membre membre;
|
||||||
|
|
||||||
/** Objets cibles de ce paiement (Cat.2 — polymorphique) */
|
/** Objets cibles de ce paiement (polymorphique) */
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@OneToMany(mappedBy = "paiement", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
@OneToMany(mappedBy = "paiement", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
@@ -115,18 +114,15 @@ public class Paiement extends BaseEntity {
|
|||||||
@JoinColumn(name = "transaction_wave_id")
|
@JoinColumn(name = "transaction_wave_id")
|
||||||
private TransactionWave transactionWave;
|
private TransactionWave transactionWave;
|
||||||
|
|
||||||
private static final AtomicLong REFERENCE_COUNTER =
|
/** Génère un numéro de référence unique */
|
||||||
new AtomicLong(System.currentTimeMillis() % 1000000000000L);
|
|
||||||
|
|
||||||
/** Méthode métier pour générer un numéro de référence unique */
|
|
||||||
public static String genererNumeroReference() {
|
public static String genererNumeroReference() {
|
||||||
return "PAY-"
|
return "PAY-"
|
||||||
+ LocalDateTime.now().getYear()
|
+ 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() {
|
public boolean isValide() {
|
||||||
return "VALIDE".equals(statutPaiement);
|
return "VALIDE".equals(statutPaiement);
|
||||||
}
|
}
|
||||||
@@ -137,12 +133,10 @@ public class Paiement extends BaseEntity {
|
|||||||
&& !"ANNULE".equals(statutPaiement);
|
&& !"ANNULE".equals(statutPaiement);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Callback JPA avant la persistance */
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
protected void onCreate() {
|
protected void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
if (numeroReference == null
|
if (numeroReference == null || numeroReference.isEmpty()) {
|
||||||
|| numeroReference.isEmpty()) {
|
|
||||||
numeroReference = genererNumeroReference();
|
numeroReference = genererNumeroReference();
|
||||||
}
|
}
|
||||||
if (statutPaiement == null) {
|
if (statutPaiement == null) {
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
package dev.lions.unionflow.server.entity;
|
package dev.lions.unionflow.server.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.validation.constraints.*;
|
||||||
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 java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -24,23 +12,11 @@ import lombok.EqualsAndHashCode;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table de liaison polymorphique entre un paiement
|
* Table de liaison polymorphique entre un paiement et son objet cible.
|
||||||
* et son objet cible.
|
|
||||||
*
|
*
|
||||||
* <p>
|
* <p>Remplace les tables dupliquées {@code paiements_cotisations},
|
||||||
* Remplace les 4 tables dupliquées
|
* {@code paiements_adhesions}, etc. par une table unique utilisant
|
||||||
* {@code paiements_cotisations},
|
* le pattern {@code (type_objet_cible, objet_cible_id)}.
|
||||||
* {@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).
|
|
||||||
*
|
*
|
||||||
* @author UnionFlow Team
|
* @author UnionFlow Team
|
||||||
* @version 3.0
|
* @version 3.0
|
||||||
@@ -49,15 +25,11 @@ import lombok.NoArgsConstructor;
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(name = "paiements_objets", indexes = {
|
@Table(name = "paiements_objets", indexes = {
|
||||||
@Index(name = "idx_po_paiement", columnList = "paiement_id"),
|
@Index(name = "idx_po_paiement", columnList = "paiement_id"),
|
||||||
@Index(name = "idx_po_objet", columnList = "type_objet_cible,"
|
@Index(name = "idx_po_objet", columnList = "type_objet_cible, objet_cible_id"),
|
||||||
+ " objet_cible_id"),
|
|
||||||
@Index(name = "idx_po_type", columnList = "type_objet_cible")
|
@Index(name = "idx_po_type", columnList = "type_objet_cible")
|
||||||
}, uniqueConstraints = {
|
}, uniqueConstraints = {
|
||||||
@UniqueConstraint(name = "uk_paiement_objet", columnNames = {
|
@UniqueConstraint(name = "uk_paiement_objet",
|
||||||
"paiement_id",
|
columnNames = {"paiement_id", "type_objet_cible", "objet_cible_id"})
|
||||||
"type_objet_cible",
|
|
||||||
"objet_cible_id"
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -73,25 +45,14 @@ public class PaiementObjet extends BaseEntity {
|
|||||||
private Paiement paiement;
|
private Paiement paiement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type de l'objet cible (code du domaine
|
* Type de l'objet cible (ex: COTISATION, ADHESION, EVENEMENT, AIDE).
|
||||||
* {@code OBJET_PAIEMENT} dans
|
|
||||||
* {@code types_reference}).
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Valeurs attendues : {@code COTISATION},
|
|
||||||
* {@code ADHESION}, {@code EVENEMENT},
|
|
||||||
* {@code AIDE}.
|
|
||||||
*/
|
*/
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Size(max = 50)
|
@Size(max = 50)
|
||||||
@Column(name = "type_objet_cible", nullable = false, length = 50)
|
@Column(name = "type_objet_cible", nullable = false, length = 50)
|
||||||
private String typeObjetCible;
|
private String typeObjetCible;
|
||||||
|
|
||||||
/**
|
/** UUID de l'objet cible. */
|
||||||
* UUID de l'objet cible (cotisation, demande
|
|
||||||
* d'adhésion, inscription événement, ou demande
|
|
||||||
* d'aide).
|
|
||||||
*/
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Column(name = "objet_cible_id", nullable = false)
|
@Column(name = "objet_cible_id", nullable = false)
|
||||||
private UUID objetCibleId;
|
private UUID objetCibleId;
|
||||||
@@ -112,13 +73,6 @@ public class PaiementObjet extends BaseEntity {
|
|||||||
@Column(name = "commentaire", length = 500)
|
@Column(name = "commentaire", length = 500)
|
||||||
private String commentaire;
|
private String commentaire;
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback JPA avant la persistance.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Initialise {@code dateApplication} si non
|
|
||||||
* renseignée.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
@PrePersist
|
@PrePersist
|
||||||
protected void onCreate() {
|
protected void onCreate() {
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package dev.lions.unionflow.server.repository;
|
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 dev.lions.unionflow.server.entity.Paiement;
|
||||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||||
import io.quarkus.panache.common.Page;
|
|
||||||
import io.quarkus.panache.common.Sort;
|
import io.quarkus.panache.common.Sort;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -14,7 +11,7 @@ import java.util.Optional;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository pour l'entité Paiement
|
* Repository pour l'entité Paiement.
|
||||||
*
|
*
|
||||||
* @author UnionFlow Team
|
* @author UnionFlow Team
|
||||||
* @version 3.0
|
* @version 3.0
|
||||||
@@ -23,90 +20,57 @@ import java.util.UUID;
|
|||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class PaiementRepository implements PanacheRepositoryBase<Paiement, UUID> {
|
public class PaiementRepository implements PanacheRepositoryBase<Paiement, UUID> {
|
||||||
|
|
||||||
/**
|
/** Trouve un paiement actif par son UUID. */
|
||||||
* Trouve un paiement par son UUID
|
|
||||||
*
|
|
||||||
* @param id UUID du paiement
|
|
||||||
* @return Paiement ou Optional.empty()
|
|
||||||
*/
|
|
||||||
public Optional<Paiement> findPaiementById(UUID id) {
|
public Optional<Paiement> findPaiementById(UUID id) {
|
||||||
return find("id = ?1 AND actif = true", id).firstResultOptional();
|
return find("id = ?1 AND actif = true", id).firstResultOptional();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Trouve un paiement par son numéro de référence. */
|
||||||
* Trouve un paiement par son numéro de référence
|
|
||||||
*
|
|
||||||
* @param numeroReference Numéro de référence
|
|
||||||
* @return Paiement ou Optional.empty()
|
|
||||||
*/
|
|
||||||
public Optional<Paiement> findByNumeroReference(String numeroReference) {
|
public Optional<Paiement> findByNumeroReference(String numeroReference) {
|
||||||
return find("numeroReference", numeroReference).firstResultOptional();
|
return find("numeroReference", numeroReference).firstResultOptional();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Tous les paiements actifs d'un membre, triés par date décroissante. */
|
||||||
* Trouve tous les paiements d'un membre
|
|
||||||
*
|
|
||||||
* @param membreId ID du membre
|
|
||||||
* @return Liste des paiements
|
|
||||||
*/
|
|
||||||
public List<Paiement> findByMembreId(UUID membreId) {
|
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();
|
.list();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Paiements actifs par statut (valeur String), triés par date décroissante. */
|
||||||
* Trouve les paiements par statut
|
public List<Paiement> findByStatut(String statut) {
|
||||||
*
|
return find(
|
||||||
* @param statut Statut du paiement
|
"statutPaiement = ?1 AND actif = true",
|
||||||
* @return Liste des paiements
|
Sort.by("datePaiement", Sort.Direction.Descending),
|
||||||
*/
|
statut)
|
||||||
public List<Paiement> findByStatut(StatutPaiement statut) {
|
|
||||||
return find("statutPaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), statut.name())
|
|
||||||
.list();
|
.list();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Paiements actifs par méthode (valeur String), triés par date décroissante. */
|
||||||
* Trouve les paiements par méthode
|
public List<Paiement> findByMethode(String methode) {
|
||||||
*
|
return find(
|
||||||
* @param methode Méthode de paiement
|
"methodePaiement = ?1 AND actif = true",
|
||||||
* @return Liste des paiements
|
Sort.by("datePaiement", Sort.Direction.Descending),
|
||||||
*/
|
methode)
|
||||||
public List<Paiement> findByMethode(MethodePaiement methode) {
|
|
||||||
return find("methodePaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), methode.name())
|
|
||||||
.list();
|
.list();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Paiements validés dans une période, triés par date de validation décroissante. */
|
||||||
* Trouve les paiements validés dans une période
|
|
||||||
*
|
|
||||||
* @param dateDebut Date de début
|
|
||||||
* @param dateFin Date de fin
|
|
||||||
* @return Liste des paiements
|
|
||||||
*/
|
|
||||||
public List<Paiement> findValidesParPeriode(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
public List<Paiement> findValidesParPeriode(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
return find(
|
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),
|
Sort.by("dateValidation", Sort.Direction.Descending),
|
||||||
StatutPaiement.VALIDE.name(),
|
|
||||||
dateDebut,
|
dateDebut,
|
||||||
dateFin)
|
dateFin)
|
||||||
.list();
|
.list();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Somme des montants validés sur une période. */
|
||||||
* Calcule le montant total des paiements validés dans une période
|
|
||||||
*
|
|
||||||
* @param dateDebut Date de début
|
|
||||||
* @param dateFin Date de fin
|
|
||||||
* @return Montant total
|
|
||||||
*/
|
|
||||||
public BigDecimal calculerMontantTotalValides(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
public BigDecimal calculerMontantTotalValides(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||||
List<Paiement> paiements = findValidesParPeriode(dateDebut, dateFin);
|
return findValidesParPeriode(dateDebut, dateFin).stream()
|
||||||
return paiements.stream()
|
|
||||||
.map(Paiement::getMontant)
|
.map(Paiement::getMontant)
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package dev.lions.unionflow.server.resource;
|
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.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.PaiementResponse;
|
||||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse;
|
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse;
|
||||||
import dev.lions.unionflow.server.service.PaiementService;
|
import dev.lions.unionflow.server.service.PaiementService;
|
||||||
@@ -16,17 +20,25 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
|||||||
import org.jboss.logging.Logger;
|
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
|
* @author UnionFlow Team
|
||||||
* @version 3.0
|
* @version 3.0
|
||||||
* @since 2025-01-29
|
* @since 2026-04-13
|
||||||
*/
|
*/
|
||||||
@Path("/api/paiements")
|
@Path("/api/paiements")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER" })
|
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER"})
|
||||||
@Tag(name = "Paiements", description = "Gestion des paiements : création, validation et suivi")
|
@Tag(name = "Paiements", description = "Paiements de cotisations — Wave Checkout et manuel")
|
||||||
public class PaiementResource {
|
public class PaiementResource {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(PaiementResource.class);
|
private static final Logger LOG = Logger.getLogger(PaiementResource.class);
|
||||||
@@ -34,56 +46,8 @@ public class PaiementResource {
|
|||||||
@Inject
|
@Inject
|
||||||
PaiementService paiementService;
|
PaiementService paiementService;
|
||||||
|
|
||||||
/**
|
// ── Lecture ───────────────────────────────────────────────────────────────
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Trouve un paiement par son ID
|
|
||||||
*
|
|
||||||
* @param id ID du paiement
|
|
||||||
* @return Paiement
|
|
||||||
*/
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/{id}")
|
@Path("/{id}")
|
||||||
public Response trouverParId(@PathParam("id") UUID id) {
|
public Response trouverParId(@PathParam("id") UUID id) {
|
||||||
@@ -92,115 +56,100 @@ public class PaiementResource {
|
|||||||
return Response.ok(result).build();
|
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
|
@GET
|
||||||
@Path("/reference/{numeroReference}")
|
@Path("/reference/{numeroReference}")
|
||||||
public Response trouverParNumeroReference(@PathParam("numeroReference") String numeroReference) {
|
public Response trouverParNumeroReference(
|
||||||
|
@PathParam("numeroReference") String numeroReference) {
|
||||||
LOG.infof("GET /api/paiements/reference/%s", numeroReference);
|
LOG.infof("GET /api/paiements/reference/%s", numeroReference);
|
||||||
PaiementResponse result = paiementService.trouverParNumeroReference(numeroReference);
|
PaiementResponse result = paiementService.trouverParNumeroReference(numeroReference);
|
||||||
return Response.ok(result).build();
|
return Response.ok(result).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Liste tous les paiements d'un membre
|
|
||||||
*
|
|
||||||
* @param membreId ID du membre
|
|
||||||
* @return Liste des paiements
|
|
||||||
*/
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/membre/{membreId}")
|
@Path("/membre/{membreId}")
|
||||||
|
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
|
||||||
public Response listerParMembre(@PathParam("membreId") UUID membreId) {
|
public Response listerParMembre(@PathParam("membreId") UUID membreId) {
|
||||||
LOG.infof("GET /api/paiements/membre/%s", membreId);
|
LOG.infof("GET /api/paiements/membre/%s", membreId);
|
||||||
List<PaiementSummaryResponse> result = paiementService.listerParMembre(membreId);
|
List<PaiementSummaryResponse> result = paiementService.listerParMembre(membreId);
|
||||||
return Response.ok(result).build();
|
return Response.ok(result).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
@GET
|
||||||
@Path("/mes-paiements/historique")
|
@Path("/mon-historique")
|
||||||
@RolesAllowed({ "MEMBRE", "ADMIN", "ADMIN_ORGANISATION" })
|
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION"})
|
||||||
public Response getMonHistoriquePaiements(
|
public Response getMonHistoriquePaiements(
|
||||||
@QueryParam("limit") @DefaultValue("5") int limit) {
|
@QueryParam("limit") @DefaultValue("20") int limit) {
|
||||||
LOG.infof("GET /api/paiements/mes-paiements/historique?limit=%d", limit);
|
LOG.infof("GET /api/paiements/mon-historique?limit=%d", limit);
|
||||||
List<PaiementSummaryResponse> result = paiementService.getMonHistoriquePaiements(limit);
|
List<PaiementSummaryResponse> result = paiementService.getMonHistoriquePaiements(limit);
|
||||||
return Response.ok(result).build();
|
return Response.ok(result).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ── Administration ────────────────────────────────────────────────────────
|
||||||
* 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
|
@POST
|
||||||
@Path("/initier-paiement-en-ligne")
|
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
|
||||||
@RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" })
|
public Response creerPaiement(@Valid CreatePaiementRequest request) {
|
||||||
public Response initierPaiementEnLigne(@Valid dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest request) {
|
LOG.infof("POST /api/paiements — référence: %s", request.numeroReference());
|
||||||
LOG.infof("POST /api/paiements/initier-paiement-en-ligne - cotisation: %s, méthode: %s",
|
PaiementResponse result = paiementService.creerPaiement(request);
|
||||||
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();
|
return Response.status(Response.Status.CREATED).entity(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
|
@POST
|
||||||
@Path("/initier-depot-epargne-en-ligne")
|
@Path("/{id}/valider")
|
||||||
@RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" })
|
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
|
||||||
public Response initierDepotEpargneEnLigne(@Valid dev.lions.unionflow.server.api.dto.paiement.request.InitierDepotEpargneRequest request) {
|
public Response validerPaiement(@PathParam("id") UUID id) {
|
||||||
LOG.infof("POST /api/paiements/initier-depot-epargne-en-ligne - compte: %s, montant: %s",
|
LOG.infof("POST /api/paiements/%s/valider", id);
|
||||||
request.compteId(), request.montant());
|
PaiementResponse result = paiementService.validerPaiement(id);
|
||||||
dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse result =
|
|
||||||
paiementService.initierDepotEpargneEnLigne(request);
|
|
||||||
return Response.status(Response.Status.CREATED).entity(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();
|
return Response.ok(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Flux Wave Checkout (QR code web) ──────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Déclare un paiement manuel (espèces, virement, chèque).
|
* Initie un paiement Wave via Checkout QR code.
|
||||||
* Le paiement est créé avec le statut EN_ATTENTE_VALIDATION.
|
* Le web encode le {@code waveLaunchUrl} en QR code, l'utilisateur le scanne
|
||||||
* Le trésorier devra le valider via une page admin.
|
* depuis l'app Wave. Après confirmation, Wave redirige vers la success URL.
|
||||||
*
|
|
||||||
* @param request Données du paiement manuel
|
|
||||||
* @return Paiement créé (statut EN_ATTENTE_VALIDATION)
|
|
||||||
*/
|
*/
|
||||||
@POST
|
@POST
|
||||||
@Path("/declarer-paiement-manuel")
|
@Path("/initier-paiement-en-ligne")
|
||||||
@RolesAllowed({ "MEMBRE", "ADMIN", "ADMIN_ORGANISATION" })
|
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION", "USER"})
|
||||||
public Response declarerPaiementManuel(@Valid dev.lions.unionflow.server.api.dto.paiement.request.DeclarerPaiementManuelRequest request) {
|
public Response initierPaiementEnLigne(@Valid InitierPaiementEnLigneRequest request) {
|
||||||
LOG.infof("POST /api/paiements/declarer-paiement-manuel - cotisation: %s, méthode: %s",
|
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());
|
request.cotisationId(), request.methodePaiement());
|
||||||
PaiementResponse result = paiementService.declarerPaiementManuel(request);
|
PaiementResponse result = paiementService.declarerPaiementManuel(request);
|
||||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package dev.lions.unionflow.server.service;
|
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.request.CreatePaiementRequest;
|
||||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse;
|
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.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.PaiementRepository;
|
||||||
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
|
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
|
||||||
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
|
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.WaveCheckoutException;
|
||||||
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutSessionResponse;
|
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutSessionResponse;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
@@ -65,12 +63,6 @@ public class PaiementService {
|
|||||||
@Inject
|
@Inject
|
||||||
CompteEpargneRepository compteEpargneRepository;
|
CompteEpargneRepository compteEpargneRepository;
|
||||||
|
|
||||||
@Inject
|
|
||||||
MembreOrganisationRepository membreOrganisationRepository;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
NotificationService notificationService;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
io.quarkus.security.identity.SecurityIdentity securityIdentity;
|
io.quarkus.security.identity.SecurityIdentity securityIdentity;
|
||||||
|
|
||||||
@@ -333,11 +325,7 @@ public class PaiementService {
|
|||||||
.build();
|
.build();
|
||||||
intentionPaiementRepository.persist(intention);
|
intentionPaiementRepository.persist(intention);
|
||||||
|
|
||||||
// Web (sans numéro de téléphone) → page HTML de confirmation ; Mobile → deep link app
|
String successUrl = base + "/api/wave-redirect/success?ref=" + intention.getId();
|
||||||
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 errorUrl = base + "/api/wave-redirect/error?ref=" + intention.getId();
|
String errorUrl = base + "/api/wave-redirect/error?ref=" + intention.getId();
|
||||||
String clientRef = intention.getId().toString();
|
String clientRef = intention.getId().toString();
|
||||||
// XOF : montant entier, pas de décimales (spec Wave)
|
// XOF : montant entier, pas de décimales (spec Wave)
|
||||||
@@ -397,113 +385,6 @@ public class PaiementService {
|
|||||||
.build();
|
.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). */
|
/** Format E.164 pour Wave (ex: 771234567 -> +225771234567). */
|
||||||
private static String toE164(String numeroTelephone) {
|
private static String toE164(String numeroTelephone) {
|
||||||
if (numeroTelephone == null || numeroTelephone.isBlank()) return null;
|
if (numeroTelephone == null || numeroTelephone.isBlank()) return null;
|
||||||
@@ -628,21 +509,13 @@ public class PaiementService {
|
|||||||
|
|
||||||
paiementRepository.persist(paiement);
|
paiementRepository.persist(paiement);
|
||||||
|
|
||||||
// Notifier l'admin de l'organisation pour validation du paiement manuel
|
// TODO: Créer une notification pour le trésorier
|
||||||
membreOrganisationRepository.findFirstByMembreId(membreConnecte.getId())
|
// notificationService.creerNotification(
|
||||||
.ifPresent(mo -> {
|
// "VALIDATION_PAIEMENT_REQUIS",
|
||||||
CreateNotificationRequest notif = CreateNotificationRequest.builder()
|
// "Validation paiement manuel requis",
|
||||||
.typeNotification("VALIDATION_PAIEMENT_REQUIS")
|
// "Le membre " + membreConnecte.getNumeroMembre() + " a déclaré un paiement manuel à valider.",
|
||||||
.priorite("HAUTE")
|
// tresorierIds
|
||||||
.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());
|
|
||||||
});
|
|
||||||
|
|
||||||
LOG.infof("Paiement manuel déclaré avec succès: ID=%s, Référence=%s (EN_ATTENTE_VALIDATION)",
|
LOG.infof("Paiement manuel déclaré avec succès: ID=%s, Référence=%s (EN_ATTENTE_VALIDATION)",
|
||||||
paiement.getId(), paiement.getNumeroReference());
|
paiement.getId(), paiement.getNumeroReference());
|
||||||
@@ -650,6 +523,69 @@ public class PaiementService {
|
|||||||
return convertToResponse(paiement);
|
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
|
// MÉTHODES PRIVÉES
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -672,6 +608,10 @@ public class PaiementService {
|
|||||||
|
|
||||||
/** Convertit une entité en Response DTO */
|
/** Convertit une entité en Response DTO */
|
||||||
private PaiementResponse convertToResponse(Paiement paiement) {
|
private PaiementResponse convertToResponse(Paiement paiement) {
|
||||||
|
if (paiement == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
PaiementResponse response = new PaiementResponse();
|
PaiementResponse response = new PaiementResponse();
|
||||||
response.setId(paiement.getId());
|
response.setId(paiement.getId());
|
||||||
response.setNumeroReference(paiement.getNumeroReference());
|
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