feat(versement): nouveau module Versement (paiements rattachés à des objets)
- Entités : Versement, VersementObjet (lien polymorphique vers cotisation/adhesion/etc.) - VersementRepository : requêtes par membre, org, période - VersementResource : endpoints REST /api/versements - VersementService : logique métier (validation, rattachement objets) - Migration V27 : ajout numeroTelephone sur versements
This commit is contained in:
171
src/main/java/dev/lions/unionflow/server/entity/Versement.java
Normal file
171
src/main/java/dev/lions/unionflow/server/entity/Versement.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* Versement — acte de régler une cotisation ou de déposer des fonds.
|
||||
*
|
||||
* <p>Un versement peut être effectué :
|
||||
* <ul>
|
||||
* <li>Via <b>Wave Mobile Money</b> : deep link natif, app Wave sur le même téléphone</li>
|
||||
* <li>Manuellement : espèces, virement, chèque → statut EN_ATTENTE_VALIDATION</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Table DB : {@code paiements} (nom hérité, conservé pour compatibilité Flyway).
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "paiements", indexes = {
|
||||
@Index(name = "idx_paiement_numero_reference", columnList = "numero_reference", unique = true),
|
||||
@Index(name = "idx_paiement_membre", columnList = "membre_id"),
|
||||
@Index(name = "idx_paiement_statut", columnList = "statut_paiement"),
|
||||
@Index(name = "idx_paiement_methode", columnList = "methode_paiement"),
|
||||
@Index(name = "idx_paiement_date", columnList = "date_paiement")
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Versement extends BaseEntity {
|
||||
|
||||
private static final AtomicLong REFERENCE_COUNTER =
|
||||
new AtomicLong(System.currentTimeMillis() % 1_000_000_000_000L);
|
||||
|
||||
// ── Identité ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** Référence unique (ex: VRS-2026-XXXXXXXXXXXX) */
|
||||
@NotBlank
|
||||
@Column(name = "numero_reference", unique = true, nullable = false, length = 50)
|
||||
private String numeroReference;
|
||||
|
||||
// ── Montant ───────────────────────────────────────────────────────────────
|
||||
|
||||
@NotNull
|
||||
@DecimalMin(value = "0.0", message = "Le montant doit être positif")
|
||||
@Digits(integer = 12, fraction = 2)
|
||||
@Column(name = "montant", nullable = false, precision = 14, scale = 2)
|
||||
private BigDecimal montant;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = "^[A-Z]{3}$", message = "Code devise ISO à 3 lettres requis")
|
||||
@Column(name = "code_devise", nullable = false, length = 3)
|
||||
private String codeDevise;
|
||||
|
||||
// ── Méthode & Statut ──────────────────────────────────────────────────────
|
||||
|
||||
/** WAVE | ESPECES | VIREMENT | CHEQUE | AUTRE */
|
||||
@NotNull
|
||||
@Column(name = "methode_paiement", nullable = false, length = 50)
|
||||
private String methodePaiement;
|
||||
|
||||
/** EN_ATTENTE | EN_COURS | CONFIRME | ECHEC | EN_ATTENTE_VALIDATION | ANNULE */
|
||||
@NotNull
|
||||
@Builder.Default
|
||||
@Column(name = "statut_paiement", nullable = false, length = 30)
|
||||
private String statutPaiement = "EN_ATTENTE";
|
||||
|
||||
// ── Dates ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@Column(name = "date_paiement")
|
||||
private LocalDateTime datePaiement;
|
||||
|
||||
@Column(name = "date_validation")
|
||||
private LocalDateTime dateValidation;
|
||||
|
||||
// ── Validation ────────────────────────────────────────────────────────────
|
||||
|
||||
@Column(name = "validateur", length = 255)
|
||||
private String validateur;
|
||||
|
||||
// ── Traçabilité ───────────────────────────────────────────────────────────
|
||||
|
||||
/** ID transaction Wave (TCN...) ou référence chèque / bordereau */
|
||||
@Column(name = "reference_externe", length = 500)
|
||||
private String referenceExterne;
|
||||
|
||||
@Column(name = "url_preuve", length = 1000)
|
||||
private String urlPreuve;
|
||||
|
||||
@Column(name = "commentaire", length = 1000)
|
||||
private String commentaire;
|
||||
|
||||
@Column(name = "ip_address", length = 45)
|
||||
private String ipAddress;
|
||||
|
||||
@Column(name = "user_agent", length = 500)
|
||||
private String userAgent;
|
||||
|
||||
// ── Téléphone Wave ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Numéro de téléphone Wave utilisé pour ce versement.
|
||||
* Pré-rempli depuis le profil du membre (même téléphone qu'UnionFlow),
|
||||
* modifiable à l'étape "Récapitulatif" avant de tapper "Payer".
|
||||
*/
|
||||
@Column(name = "numero_telephone", length = 20)
|
||||
private String numeroTelephone;
|
||||
|
||||
// ── Relations ─────────────────────────────────────────────────────────────
|
||||
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "membre_id", nullable = false)
|
||||
private Membre membre;
|
||||
|
||||
@JsonIgnore
|
||||
@OneToMany(mappedBy = "versement", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
@Builder.Default
|
||||
private List<VersementObjet> versementsObjets = new ArrayList<>();
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "transaction_wave_id")
|
||||
private TransactionWave transactionWave;
|
||||
|
||||
// ── Méthodes métier ───────────────────────────────────────────────────────
|
||||
|
||||
/** Génère une référence unique : VRS-YYYY-XXXXXXXXXXXX */
|
||||
public static String genererNumeroReference() {
|
||||
return "VRS-"
|
||||
+ LocalDateTime.now().getYear()
|
||||
+ "-"
|
||||
+ String.format("%012d", REFERENCE_COUNTER.incrementAndGet() % 1_000_000_000_000L);
|
||||
}
|
||||
|
||||
/** Vrai si le versement est confirmé (Wave) ou validé (manuel) */
|
||||
public boolean isConfirme() {
|
||||
return "CONFIRME".equals(statutPaiement) || "VALIDE".equals(statutPaiement);
|
||||
}
|
||||
|
||||
/** Vrai si le versement peut encore être modifié ou annulé */
|
||||
public boolean peutEtreModifie() {
|
||||
return !"CONFIRME".equals(statutPaiement)
|
||||
&& !"VALIDE".equals(statutPaiement)
|
||||
&& !"ANNULE".equals(statutPaiement);
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (numeroReference == null || numeroReference.isBlank()) {
|
||||
numeroReference = genererNumeroReference();
|
||||
}
|
||||
if (statutPaiement == null) {
|
||||
statutPaiement = "EN_ATTENTE";
|
||||
}
|
||||
if (datePaiement == null) {
|
||||
datePaiement = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* Liaison polymorphique entre un versement et son objet cible.
|
||||
*
|
||||
* <p>Remplace les 4 tables dupliquées (paiements_cotisations, paiements_adhesions,
|
||||
* paiements_evenements, paiements_aides) par une table unique utilisant le pattern
|
||||
* {@code (typeObjetCible, objetCibleId)}.
|
||||
*
|
||||
* <p>Table DB : {@code paiements_objets} (nom hérité, conservé pour compatibilité Flyway).
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@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")
|
||||
}, uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_paiement_objet", columnNames = {
|
||||
"paiement_id", "type_objet_cible", "objet_cible_id"
|
||||
})
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class VersementObjet extends BaseEntity {
|
||||
|
||||
/** Versement parent. */
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "paiement_id", nullable = false)
|
||||
private Versement versement;
|
||||
|
||||
/**
|
||||
* Type de l'objet cible (domaine {@code OBJET_PAIEMENT}).
|
||||
* Valeurs : 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, adhésion, inscription, aide). */
|
||||
@NotNull
|
||||
@Column(name = "objet_cible_id", nullable = false)
|
||||
private UUID objetCibleId;
|
||||
|
||||
/** Montant affecté à 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;
|
||||
|
||||
@Column(name = "date_application")
|
||||
private LocalDateTime dateApplication;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "commentaire", length = 500)
|
||||
private String commentaire;
|
||||
|
||||
@Override
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (dateApplication == null) {
|
||||
dateApplication = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user