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; /** * Référentiel comptable applicable à cette organisation. * *
Détermine quel plan comptable est appliqué et quels états financiers sont générés
* (bilan, compte de résultat, annexes). Mappage par défaut depuis {@code typeOrganisation}
* via {@link ReferentielComptable#defaultFor(String)} ; l'admin peut overrider manuellement.
*
* @since 2026-04-25 — découverte SYCEBNL (11ᵉ Acte uniforme OHADA en vigueur 1er jan 2024)
*/
@Enumerated(EnumType.STRING)
@Column(name = "referentiel_comptable", nullable = false, length = 20)
@Builder.Default
private ReferentielComptable referentielComptable = ReferentielComptable.SYSCOHADA;
/**
* UUID du membre désigné comme Compliance Officer de l'organisation (rôle obligatoire selon
* Instruction BCEAO 001-03-2025). Doit être rattaché à la direction générale, distinct du
* trésorier (séparation des pouvoirs).
*
* @since 2026-04-25 — Instruction BCEAO 001-03-2025 (LBC/FT)
*/
@Column(name = "compliance_officer_id")
private UUID complianceOfficerId;
// Relations
/** Adhésions des membres à cette organisation */
@JsonIgnore
@OneToMany(mappedBy = "organisation", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List