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