package dev.lions.unionflow.server.entity; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import jakarta.validation.constraints.*; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.UUID; import lombok.*; /** * Identité globale unique d'un utilisateur UnionFlow. * *

* Un utilisateur possède un seul compte sur toute la plateforme. * Ses adhésions aux organisations sont gérées dans {@link MembreOrganisation}. * *

* Table : {@code utilisateurs} */ @Entity @Table(name = "utilisateurs", indexes = { @Index(name = "idx_utilisateur_email", columnList = "email", unique = true), @Index(name = "idx_utilisateur_numero", columnList = "numero_membre", unique = true), @Index(name = "idx_utilisateur_keycloak", columnList = "keycloak_id", unique = true), @Index(name = "idx_utilisateur_actif", columnList = "actif"), @Index(name = "idx_utilisateur_statut", columnList = "statut_compte") }) @Data @NoArgsConstructor @AllArgsConstructor @Builder @EqualsAndHashCode(callSuper = true) public class Membre extends BaseEntity { /** Identifiant Keycloak (UUID du compte OIDC) */ @Column(name = "keycloak_id", unique = true) private UUID keycloakId; /** Numéro de membre — unique globalement sur toute la plateforme */ @NotBlank @Column(name = "numero_membre", unique = true, nullable = false, length = 20) private String numeroMembre; @NotBlank @Column(name = "prenom", nullable = false, length = 100) private String prenom; @NotBlank @Column(name = "nom", nullable = false, length = 100) private String nom; @Email @NotBlank @Column(name = "email", unique = true, nullable = false, length = 255) private String email; @Column(name = "telephone", length = 20) private String telephone; /** Token FCM pour les notifications push Firebase. NULL si l'app mobile n'est pas installée ou si le membre a refusé les notifications. */ @Column(name = "fcm_token", length = 500) private String fcmToken; /** * Numéro CMU (Couverture Maladie Universelle) Côte d'Ivoire — auto-déclaré par le membre. * *

Obligatoire pour les organisations de type {@code MUTUELLE_SANTE} (Loi 2014-131 * exige enrôlement CMU comme préalable à toute mutuelle complémentaire). Format CNAM : * 11 caractères alphanumériques. La vérification de la validité se fait manuellement * (admin) faute d'API publique CNAM disponible au 2026-04-25. * * @since 2026-04-25 — passage CMU à cotisation obligatoire 1er jan 2026 */ @Pattern(regexp = "^[A-Z0-9]{11}$|^$", message = "Le numéro CMU doit faire 11 caractères alphanumériques majuscules") @Column(name = "numero_cmu", length = 11) private String numeroCMU; /** * Pays de résidence (ISO-3, ex: FRA, USA, CAN). Différent de {@code nationalite} : * un Ivoirien (CIV) résidant en France a paysResidence=FRA. NULL ou CIV = résident UEMOA. * * @since 2026-04-25 (P2-NEW-7) */ @Pattern(regexp = "^[A-Z]{3}$|^$", message = "Pays résidence doit être un code ISO-3") @Column(name = "pays_residence", length = 3) private String paysResidence; /** Numéro de passeport pour non-résidents (CNI insuffisante hors UEMOA). */ @Column(name = "numero_passeport", length = 50) private String numeroPasseport; /** NIF/SSN/SIN — reporting fiscal accord bilatéral CI ↔ pays résidence. */ @Column(name = "numero_fiscal_etranger", length = 50) private String numeroFiscalEtranger; /** TRUE si le membre est diaspora (résidence ≠ UEMOA). */ @Builder.Default @Column(name = "est_diaspora", nullable = false) private Boolean estDiaspora = false; /** Devise préférée pour affichages et notifications (XOF par défaut). */ @Builder.Default @Column(name = "devise_preferee", nullable = false, length = 3) private String devisePreferee = "XOF"; @Pattern(regexp = "^\\+[1-9][0-9]{6,14}$", message = "Le numéro Wave doit être au format international E.164 (ex: +22507XXXXXXXX)") @Column(name = "telephone_wave", length = 20) private String telephoneWave; @NotNull @Column(name = "date_naissance", nullable = false) private LocalDate dateNaissance; @Column(name = "profession", length = 100) private String profession; @Column(name = "photo_url", length = 500) private String photoUrl; @Builder.Default @Column(name = "statut_compte", nullable = false, length = 30) private String statutCompte = "EN_ATTENTE_VALIDATION"; /** Vrai si le membre n'a jamais changé son mot de passe généré par l'admin. */ @Builder.Default @Column(name = "premiere_connexion", nullable = false) private Boolean premiereConnexion = true; /** * Statut matrimonial (domaine * {@code STATUT_MATRIMONIAL} dans * {@code types_reference}). */ @Column(name = "statut_matrimonial", length = 50) private String statutMatrimonial; /** Nationalité. */ @Column(name = "nationalite", length = 100) private String nationalite; /** * Type de pièce d'identité (domaine * {@code TYPE_IDENTITE} dans * {@code types_reference}). */ @Column(name = "type_identite", length = 50) private String typeIdentite; /** Numéro de la pièce d'identité. */ @Column(name = "numero_identite", length = 100) private String numeroIdentite; /** Notes / biographie libre du membre. */ @Column(name = "notes", length = 1000) private String notes; /** Niveau de vigilance KYC LCB-FT (SIMPLIFIE, RENFORCE). */ @Column(name = "niveau_vigilance_kyc", length = 20) private String niveauVigilanceKyc; /** Statut de vérification d'identité (NON_VERIFIE, EN_COURS, VERIFIE, REFUSE). */ @Column(name = "statut_kyc", length = 20) private String statutKyc; /** Date de dernière vérification d'identité. */ @Column(name = "date_verification_identite") private LocalDate dateVerificationIdentite; // ── Relations ──────────────────────────────────────────────────────────── /** Adhésions à des organisations */ @JsonIgnore @OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default private List membresOrganisations = new ArrayList<>(); @JsonIgnore @OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default private List adresses = new ArrayList<>(); @JsonIgnore @OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default private List comptesWave = new ArrayList<>(); @JsonIgnore @OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @Builder.Default private List paiements = new ArrayList<>(); // ── Méthodes métier ─────────────────────────────────────────────────────── public String getNomComplet() { return prenom + " " + nom; } public boolean isMajeur() { return dateNaissance != null && dateNaissance.isBefore(LocalDate.now().minusYears(18)); } public int getAge() { return dateNaissance != null ? LocalDate.now().getYear() - dateNaissance.getYear() : 0; } @PrePersist protected void onCreate() { super.onCreate(); if (statutCompte == null) { statutCompte = "EN_ATTENTE_VALIDATION"; } } }