package dev.lions.unionflow.server.entity; import jakarta.persistence.*; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; /** * Entité Cotisation avec UUID Représente une cotisation d'un membre à son * organisation * * @author UnionFlow Team * @version 2.0 * @since 2025-01-16 */ @Entity @Table(name = "cotisations", indexes = { @Index(name = "idx_cotisation_membre", columnList = "membre_id"), @Index(name = "idx_cotisation_reference", columnList = "numero_reference", unique = true), @Index(name = "idx_cotisation_statut", columnList = "statut"), @Index(name = "idx_cotisation_echeance", columnList = "date_echeance"), @Index(name = "idx_cotisation_type", columnList = "type_cotisation"), @Index(name = "idx_cotisation_annee_mois", columnList = "annee, mois") }) @Data @NoArgsConstructor @AllArgsConstructor @Builder @EqualsAndHashCode(callSuper = true) public class Cotisation extends BaseEntity { @NotBlank @Column(name = "numero_reference", unique = true, nullable = false, length = 50) private String numeroReference; @NotNull @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "membre_id", nullable = false) private Membre membre; /** Organisation pour laquelle la cotisation est due */ @NotNull @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "organisation_id", nullable = false) private Organisation organisation; /** Intention de paiement Wave associée (null si cotisation en attente) */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "intention_paiement_id") private IntentionPaiement intentionPaiement; @NotBlank @Column(name = "type_cotisation", nullable = false, length = 50) private String typeCotisation; @NotBlank @Column(name = "libelle", nullable = false, length = 100) private String libelle; @NotNull @DecimalMin(value = "0.0", message = "Le montant dû doit être positif") @Digits(integer = 10, fraction = 2) @Column(name = "montant_du", nullable = false, precision = 12, scale = 2) private BigDecimal montantDu; @Builder.Default @DecimalMin(value = "0.0", message = "Le montant payé doit être positif") @Digits(integer = 10, fraction = 2) @Column(name = "montant_paye", nullable = false, precision = 12, scale = 2) private BigDecimal montantPaye = BigDecimal.ZERO; @NotBlank @Pattern(regexp = "^[A-Z]{3}$", message = "Le code devise doit être un code ISO à 3 lettres") @Column(name = "code_devise", nullable = false, length = 3) private String codeDevise; @NotBlank @Pattern(regexp = "^(EN_ATTENTE|PAYEE|EN_RETARD|PARTIELLEMENT_PAYEE|ANNULEE)$") @Column(name = "statut", nullable = false, length = 30) private String statut; @NotNull @Column(name = "date_echeance", nullable = false) private LocalDate dateEcheance; @Column(name = "date_paiement") private LocalDateTime datePaiement; @Size(max = 500) @Column(name = "description", length = 500) private String description; @Size(max = 20) @Column(name = "periode", length = 20) private String periode; @NotNull @Min(value = 2020, message = "L'année doit être supérieure à 2020") @Max(value = 2100, message = "L'année doit être inférieure à 2100") @Column(name = "annee", nullable = false) private Integer annee; @Min(value = 1, message = "Le mois doit être entre 1 et 12") @Max(value = 12, message = "Le mois doit être entre 1 et 12") @Column(name = "mois") private Integer mois; @Size(max = 1000) @Column(name = "observations", length = 1000) private String observations; @Builder.Default @Column(name = "recurrente", nullable = false) private Boolean recurrente = false; @Builder.Default @Min(value = 0, message = "Le nombre de rappels doit être positif") @Column(name = "nombre_rappels", nullable = false) private Integer nombreRappels = 0; @Column(name = "date_dernier_rappel") private LocalDateTime dateDernierRappel; @Column(name = "valide_par_id") private UUID valideParId; @Size(max = 100) @Column(name = "nom_validateur", length = 100) private String nomValidateur; @Column(name = "date_validation") private LocalDateTime dateValidation; /** Méthode métier pour calculer le montant restant à payer */ public BigDecimal getMontantRestant() { if (montantDu == null || montantPaye == null) { return BigDecimal.ZERO; } return montantDu.subtract(montantPaye); } /** Méthode métier pour vérifier si la cotisation est entièrement payée */ public boolean isEntierementPayee() { return getMontantRestant().compareTo(BigDecimal.ZERO) <= 0; } /** Méthode métier pour vérifier si la cotisation est en retard */ public boolean isEnRetard() { return dateEcheance != null && dateEcheance.isBefore(LocalDate.now()) && !isEntierementPayee(); } private static final AtomicLong REFERENCE_COUNTER = new AtomicLong(System.currentTimeMillis() % 100000000L); /** Méthode métier pour générer un numéro de référence unique */ public static String genererNumeroReference() { return "COT-" + LocalDate.now().getYear() + "-" + String.format("%08d", REFERENCE_COUNTER.incrementAndGet() % 100000000L); } /** Callback JPA avant la persistance */ @PrePersist protected void onCreate() { super.onCreate(); // Appelle le onCreate de BaseEntity if (numeroReference == null || numeroReference.isEmpty()) { numeroReference = genererNumeroReference(); } if (codeDevise == null) { codeDevise = "XOF"; } if (statut == null) { statut = "EN_ATTENTE"; } if (montantPaye == null) { montantPaye = BigDecimal.ZERO; } if (nombreRappels == null) { nombreRappels = 0; } if (recurrente == null) { recurrente = false; } } }