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.LocalDate; import java.time.Period; import java.util.ArrayList; import java.util.List; import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; /** * Entité Organisation avec UUID Représente une organisation (Lions Club, * Association, * Coopérative, etc.) * * @author UnionFlow Team * @version 2.0 * @since 2025-01-16 */ @Entity @Table(name = "organisations", indexes = { @Index(name = "idx_organisation_nom", columnList = "nom"), @Index(name = "idx_organisation_email", columnList = "email", unique = true), @Index(name = "idx_organisation_statut", columnList = "statut"), @Index(name = "idx_organisation_type", columnList = "type_organisation"), @Index(name = "idx_organisation_parente", columnList = "organisation_parente_id"), @Index(name = "idx_organisation_numero_enregistrement", columnList = "numero_enregistrement", unique = true) }) @Data @NoArgsConstructor @AllArgsConstructor @Builder @EqualsAndHashCode(callSuper = true) public class Organisation extends BaseEntity { @NotBlank @Column(name = "nom", nullable = false, length = 200) private String nom; @Column(name = "nom_court", length = 50) private String nomCourt; @NotBlank @Column(name = "type_organisation", nullable = false, length = 50) private String typeOrganisation; @NotBlank @Column(name = "statut", nullable = false, length = 50) private String statut; @Column(name = "description", length = 2000) private String description; @Column(name = "date_fondation") private LocalDate dateFondation; @Column(name = "numero_enregistrement", unique = true, length = 100) private String numeroEnregistrement; // Informations de contact @Email @NotBlank @Column(name = "email", unique = true, nullable = false, length = 255) private String email; @Column(name = "telephone", length = 20) private String telephone; @Column(name = "telephone_secondaire", length = 20) private String telephoneSecondaire; @Email @Column(name = "email_secondaire", length = 255) private String emailSecondaire; // Adresse principale (champs dénormalisés pour performance) @Column(name = "adresse", length = 500) private String adresse; @Column(name = "ville", length = 100) private String ville; @Column(name = "region", length = 100) private String region; @Column(name = "pays", length = 100) private String pays; @Column(name = "code_postal", length = 20) private String codePostal; // Coordonnées géographiques @DecimalMin(value = "-90.0", message = "La latitude doit être comprise entre -90 et 90") @DecimalMax(value = "90.0", message = "La latitude doit être comprise entre -90 et 90") @Digits(integer = 3, fraction = 6) @Column(name = "latitude", precision = 9, scale = 6) private BigDecimal latitude; @DecimalMin(value = "-180.0", message = "La longitude doit être comprise entre -180 et 180") @DecimalMax(value = "180.0", message = "La longitude doit être comprise entre -180 et 180") @Digits(integer = 3, fraction = 6) @Column(name = "longitude", precision = 9, scale = 6) private BigDecimal longitude; // Web et réseaux sociaux @Column(name = "site_web", length = 500) private String siteWeb; @Column(name = "logo", length = 500) private String logo; @Column(name = "reseaux_sociaux", length = 1000) private String reseauxSociaux; // ── Hiérarchie ────────────────────────────────────────────────────────────── /** Organisation parente — FK propre (null = organisation racine) */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "organisation_parente_id") private Organisation organisationParente; @Builder.Default @Column(name = "niveau_hierarchique", nullable = false) private Integer niveauHierarchique = 0; /** * TRUE si c'est l'organisation racine qui porte la souscription SaaS * pour toute sa hiérarchie. */ @Builder.Default @Column(name = "est_organisation_racine", nullable = false) private Boolean estOrganisationRacine = true; /** * Chemin hiérarchique complet — ex: /uuid-racine/uuid-intermediate/uuid-feuille * Permet des requêtes récursives optimisées sans CTE. */ @Column(name = "chemin_hierarchique", length = 2000) private String cheminHierarchique; // Statistiques @Builder.Default @Column(name = "nombre_membres", nullable = false) private Integer nombreMembres = 0; @Builder.Default @Column(name = "nombre_administrateurs", nullable = false) private Integer nombreAdministrateurs = 0; // Finances @DecimalMin(value = "0.0", message = "Le budget annuel doit être positif") @Digits(integer = 12, fraction = 2) @Column(name = "budget_annuel", precision = 14, scale = 2) private BigDecimal budgetAnnuel; @Builder.Default @Column(name = "devise", length = 3) private String devise = "XOF"; @Builder.Default @Column(name = "cotisation_obligatoire", nullable = false) private Boolean cotisationObligatoire = false; @DecimalMin(value = "0.0", message = "Le montant de cotisation doit être positif") @Digits(integer = 10, fraction = 2) @Column(name = "montant_cotisation_annuelle", precision = 12, scale = 2) private BigDecimal montantCotisationAnnuelle; // Informations complémentaires @Column(name = "objectifs", length = 2000) private String objectifs; @Column(name = "activites_principales", length = 2000) private String activitesPrincipales; @Column(name = "certifications", length = 500) private String certifications; @Column(name = "partenaires", length = 1000) private String partenaires; @Column(name = "notes", length = 1000) private String notes; // Paramètres @Builder.Default @Column(name = "organisation_publique", nullable = false) private Boolean organisationPublique = true; @Builder.Default @Column(name = "accepte_nouveaux_membres", nullable = false) private Boolean accepteNouveauxMembres = true; /** Catégorie du type d'organisation (ASSOCIATIF, FINANCIER_SOLIDAIRE, RELIGIEUX, PROFESSIONNEL, RESEAU_FEDERATION) */ @Column(name = "categorie_type", length = 50) private String categorieType; /** ID de l'Organization Keycloak 26 correspondante — null si pas encore migrée. */ @Column(name = "keycloak_org_id") private UUID keycloakOrgId; /** Modules activés pour cette organisation (liste CSV, ex: "MEMBRES,COTISATIONS,TONTINE") */ @Column(name = "modules_actifs", length = 1000) private String modulesActifs; // Relations /** Adhésions des membres à cette organisation */ @JsonIgnore @OneToMany(mappedBy = "organisation", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default private List membresOrganisations = new ArrayList<>(); @JsonIgnore @OneToMany(mappedBy = "organisation", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default private List adresses = new ArrayList<>(); @JsonIgnore @OneToMany(mappedBy = "organisation", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default private List comptesWave = new ArrayList<>(); /** Méthode métier pour obtenir le nom complet avec sigle */ public String getNomComplet() { if (nomCourt != null && !nomCourt.isEmpty()) { return nom + " (" + nomCourt + ")"; } return nom; } /** Méthode métier pour calculer l'ancienneté en années */ public int getAncienneteAnnees() { if (dateFondation == null) { return 0; } return Period.between(dateFondation, LocalDate.now()).getYears(); } /** * Méthode métier pour vérifier si l'organisation est récente (moins de 2 ans) */ public boolean isRecente() { return getAncienneteAnnees() < 2; } /** Méthode métier pour vérifier si l'organisation est active */ public boolean isActive() { return "ACTIVE".equals(statut) && Boolean.TRUE.equals(getActif()); } /** Méthode métier pour ajouter un membre */ public void ajouterMembre() { if (nombreMembres == null) { nombreMembres = 0; } nombreMembres++; } /** Méthode métier pour retirer un membre */ public void retirerMembre() { if (nombreMembres != null && nombreMembres > 0) { nombreMembres--; } } /** Méthode métier pour activer l'organisation */ public void activer(String utilisateur) { this.statut = "ACTIVE"; this.setActif(true); marquerCommeModifie(utilisateur); } /** Méthode métier pour suspendre l'organisation */ public void suspendre(String utilisateur) { this.statut = "SUSPENDUE"; this.accepteNouveauxMembres = false; marquerCommeModifie(utilisateur); } /** Méthode métier pour dissoudre l'organisation */ public void dissoudre(String utilisateur) { this.statut = "DISSOUTE"; this.setActif(false); this.accepteNouveauxMembres = false; marquerCommeModifie(utilisateur); } /** Callback JPA avant la persistance */ @PrePersist protected void onCreate() { super.onCreate(); // Appelle le onCreate de BaseEntity if (statut == null) { statut = "ACTIVE"; } if (typeOrganisation == null) { typeOrganisation = "ASSOCIATION"; } if (devise == null) { devise = "XOF"; } if (niveauHierarchique == null) { niveauHierarchique = 0; } if (estOrganisationRacine == null) { estOrganisationRacine = (organisationParente == null); } if (nombreMembres == null) { nombreMembres = 0; } if (nombreAdministrateurs == null) { nombreAdministrateurs = 0; } if (organisationPublique == null) { organisationPublique = true; } if (accepteNouveauxMembres == null) { accepteNouveauxMembres = true; } if (cotisationObligatoire == null) { cotisationObligatoire = false; } } }