diff --git a/src/main/java/dev/lions/unionflow/server/entity/AlertConfiguration.java b/src/main/java/dev/lions/unionflow/server/entity/AlertConfiguration.java new file mode 100644 index 0000000..4edd6f7 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/AlertConfiguration.java @@ -0,0 +1,113 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +/** + * Entité singleton pour la configuration des alertes système. + * Une seule ligne en base de données. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@Entity +@Table(name = "alert_configuration") +@Getter +@Setter +public class AlertConfiguration extends BaseEntity { + + /** + * Alerte CPU activée + */ + @Column(name = "cpu_high_alert_enabled", nullable = false) + private Boolean cpuHighAlertEnabled = true; + + /** + * Seuil CPU en pourcentage (0-100) + */ + @Column(name = "cpu_threshold_percent", nullable = false) + private Integer cpuThresholdPercent = 80; + + /** + * Durée en minutes avant déclenchement alerte CPU + */ + @Column(name = "cpu_duration_minutes", nullable = false) + private Integer cpuDurationMinutes = 5; + + /** + * Alerte mémoire faible activée + */ + @Column(name = "memory_low_alert_enabled", nullable = false) + private Boolean memoryLowAlertEnabled = true; + + /** + * Seuil mémoire en pourcentage (0-100) + */ + @Column(name = "memory_threshold_percent", nullable = false) + private Integer memoryThresholdPercent = 85; + + /** + * Alerte erreur critique activée + */ + @Column(name = "critical_error_alert_enabled", nullable = false) + private Boolean criticalErrorAlertEnabled = true; + + /** + * Alerte erreur activée + */ + @Column(name = "error_alert_enabled", nullable = false) + private Boolean errorAlertEnabled = true; + + /** + * Alerte échec de connexion activée + */ + @Column(name = "connection_failure_alert_enabled", nullable = false) + private Boolean connectionFailureAlertEnabled = true; + + /** + * Seuil d'échecs de connexion + */ + @Column(name = "connection_failure_threshold", nullable = false) + private Integer connectionFailureThreshold = 100; + + /** + * Fenêtre temporelle en minutes pour les échecs de connexion + */ + @Column(name = "connection_failure_window_minutes", nullable = false) + private Integer connectionFailureWindowMinutes = 5; + + /** + * Notifications par email activées + */ + @Column(name = "email_notifications_enabled", nullable = false) + private Boolean emailNotificationsEnabled = true; + + /** + * Notifications push activées + */ + @Column(name = "push_notifications_enabled", nullable = false) + private Boolean pushNotificationsEnabled = false; + + /** + * Notifications SMS activées + */ + @Column(name = "sms_notifications_enabled", nullable = false) + private Boolean smsNotificationsEnabled = false; + + /** + * Liste des emails destinataires des alertes (séparés par virgule) + */ + @Column(name = "alert_email_recipients", length = 1000) + private String alertEmailRecipients = "admin@unionflow.test"; + + /** + * S'assurer qu'il n'y a qu'une seule configuration + */ + @PrePersist + @PreUpdate + protected void ensureSingleton() { + // La logique singleton sera gérée par le repository + } +} diff --git a/src/main/java/dev/lions/unionflow/server/entity/AlerteLcbFt.java b/src/main/java/dev/lions/unionflow/server/entity/AlerteLcbFt.java new file mode 100644 index 0000000..486c489 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/AlerteLcbFt.java @@ -0,0 +1,122 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * Entité représentant une alerte LCB-FT (Lutte Contre le Blanchiment et Financement du Terrorisme). + * Les alertes sont générées automatiquement lors de transactions dépassant les seuils configurés. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@Entity +@Table(name = "alertes_lcb_ft", indexes = { + @Index(name = "idx_alerte_lcb_ft_organisation", columnList = "organisation_id"), + @Index(name = "idx_alerte_lcb_ft_type", columnList = "type_alerte"), + @Index(name = "idx_alerte_lcb_ft_date", columnList = "date_alerte"), + @Index(name = "idx_alerte_lcb_ft_traitee", columnList = "traitee") +}) +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AlerteLcbFt extends BaseEntity { + + /** + * Organisation concernée par l'alerte + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id", nullable = false) + private Organisation organisation; + + /** + * Membre concerné par l'alerte + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id") + private Membre membre; + + /** + * Type d'alerte : SEUIL_DEPASSE, JUSTIFICATION_MANQUANTE, etc. + */ + @Column(name = "type_alerte", nullable = false, length = 50) + private String typeAlerte; + + /** + * Date et heure de génération de l'alerte + */ + @Column(name = "date_alerte", nullable = false) + private LocalDateTime dateAlerte; + + /** + * Description de l'alerte + */ + @Column(name = "description", length = 500) + private String description; + + /** + * Détails supplémentaires (JSON ou texte) + */ + @Column(name = "details", columnDefinition = "TEXT") + private String details; + + /** + * Montant de la transaction ayant généré l'alerte + */ + @Column(name = "montant", precision = 15, scale = 2) + private BigDecimal montant; + + /** + * Seuil qui a été dépassé + */ + @Column(name = "seuil", precision = 15, scale = 2) + private BigDecimal seuil; + + /** + * Type d'opération : DEPOT, RETRAIT, TRANSFERT, etc. + */ + @Column(name = "type_operation", length = 50) + private String typeOperation; + + /** + * Référence de la transaction concernée (UUID) + */ + @Column(name = "transaction_ref", length = 100) + private String transactionRef; + + /** + * Niveau de gravité : INFO, WARNING, CRITICAL + */ + @Column(name = "severite", nullable = false, length = 20) + private String severite; + + /** + * Indique si l'alerte a été traitée + */ + @Column(name = "traitee", nullable = false) + private Boolean traitee = false; + + /** + * Date de traitement de l'alerte + */ + @Column(name = "date_traitement") + private LocalDateTime dateTraitement; + + /** + * Utilisateur ayant traité l'alerte + */ + @Column(name = "traite_par", length = 100) + private String traitePar; + + /** + * Commentaire sur le traitement + */ + @Column(name = "commentaire_traitement", columnDefinition = "TEXT") + private String commentaireTraitement; +} diff --git a/src/main/java/dev/lions/unionflow/server/entity/SystemAlert.java b/src/main/java/dev/lions/unionflow/server/entity/SystemAlert.java new file mode 100644 index 0000000..f069066 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/SystemAlert.java @@ -0,0 +1,118 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** + * Entité pour les alertes système. + * Enregistre les alertes de seuils dépassés, erreurs critiques, etc. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@Entity +@Table(name = "system_alerts", indexes = { + @Index(name = "idx_system_alert_timestamp", columnList = "timestamp"), + @Index(name = "idx_system_alert_level", columnList = "level"), + @Index(name = "idx_system_alert_acknowledged", columnList = "acknowledged"), + @Index(name = "idx_system_alert_source", columnList = "source") +}) +@Getter +@Setter +public class SystemAlert extends BaseEntity { + + /** + * Niveau de l'alerte (CRITICAL, ERROR, WARNING, INFO) + */ + @Column(name = "level", nullable = false, length = 20) + private String level; + + /** + * Titre court de l'alerte + */ + @Column(name = "title", nullable = false, length = 255) + private String title; + + /** + * Message détaillé de l'alerte + */ + @Column(name = "message", nullable = false, length = 1000) + private String message; + + /** + * Date/heure de création de l'alerte + */ + @Column(name = "timestamp", nullable = false) + private LocalDateTime timestamp; + + /** + * Alerte acquittée ou non + */ + @Column(name = "acknowledged", nullable = false) + private Boolean acknowledged = false; + + /** + * Email de l'utilisateur ayant acquitté l'alerte + */ + @Column(name = "acknowledged_by", length = 255) + private String acknowledgedBy; + + /** + * Date/heure d'acquittement + */ + @Column(name = "acknowledged_at") + private LocalDateTime acknowledgedAt; + + /** + * Source de l'alerte (CPU, MEMORY, DISK, DATABASE, etc.) + */ + @Column(name = "source", length = 100) + private String source; + + /** + * Type d'alerte (THRESHOLD, INFO, ERROR, etc.) + */ + @Column(name = "alert_type", length = 50) + private String alertType; + + /** + * Valeur actuelle ayant déclenché l'alerte + */ + @Column(name = "current_value") + private Double currentValue; + + /** + * Valeur seuil dépassée + */ + @Column(name = "threshold_value") + private Double thresholdValue; + + /** + * Unité de mesure (%, MB, GB, ms, etc.) + */ + @Column(name = "unit", length = 20) + private String unit; + + /** + * Actions recommandées pour résoudre l'alerte + */ + @Column(name = "recommended_actions", columnDefinition = "TEXT") + private String recommendedActions; + + /** + * Initialisation automatique du timestamp + */ + @PrePersist + protected void onCreate() { + if (timestamp == null) { + timestamp = LocalDateTime.now(); + } + if (acknowledged == null) { + acknowledged = false; + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/AlertConfigurationRepository.java b/src/main/java/dev/lions/unionflow/server/repository/AlertConfigurationRepository.java new file mode 100644 index 0000000..9dc9ef1 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/AlertConfigurationRepository.java @@ -0,0 +1,101 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.AlertConfiguration; +import io.quarkus.arc.Unremovable; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; + +import java.util.Optional; + +/** + * Repository pour l'entité AlertConfiguration (singleton) + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@ApplicationScoped +@Unremovable +public class AlertConfigurationRepository extends BaseRepository { + + public AlertConfigurationRepository() { + super(AlertConfiguration.class); + } + + /** + * Récupérer la configuration unique des alertes. + * Crée une configuration par défaut si elle n'existe pas. + */ + public AlertConfiguration getConfiguration() { + TypedQuery query = entityManager.createQuery( + "SELECT c FROM AlertConfiguration c", + AlertConfiguration.class + ); + query.setMaxResults(1); + + Optional config = query.getResultList().stream().findFirst(); + + if (config.isPresent()) { + return config.get(); + } else { + // Créer une configuration par défaut + AlertConfiguration defaultConfig = new AlertConfiguration(); + persist(defaultConfig); + return defaultConfig; + } + } + + /** + * Mettre à jour la configuration des alertes + */ + public AlertConfiguration updateConfiguration(AlertConfiguration config) { + AlertConfiguration existing = getConfiguration(); + + // Mettre à jour tous les champs + existing.setCpuHighAlertEnabled(config.getCpuHighAlertEnabled()); + existing.setCpuThresholdPercent(config.getCpuThresholdPercent()); + existing.setCpuDurationMinutes(config.getCpuDurationMinutes()); + existing.setMemoryLowAlertEnabled(config.getMemoryLowAlertEnabled()); + existing.setMemoryThresholdPercent(config.getMemoryThresholdPercent()); + existing.setCriticalErrorAlertEnabled(config.getCriticalErrorAlertEnabled()); + existing.setErrorAlertEnabled(config.getErrorAlertEnabled()); + existing.setConnectionFailureAlertEnabled(config.getConnectionFailureAlertEnabled()); + existing.setConnectionFailureThreshold(config.getConnectionFailureThreshold()); + existing.setConnectionFailureWindowMinutes(config.getConnectionFailureWindowMinutes()); + existing.setEmailNotificationsEnabled(config.getEmailNotificationsEnabled()); + existing.setPushNotificationsEnabled(config.getPushNotificationsEnabled()); + existing.setSmsNotificationsEnabled(config.getSmsNotificationsEnabled()); + existing.setAlertEmailRecipients(config.getAlertEmailRecipients()); + + persist(existing); + return existing; + } + + /** + * Vérifier si les alertes CPU sont activées + */ + public boolean isCpuAlertEnabled() { + return getConfiguration().getCpuHighAlertEnabled(); + } + + /** + * Vérifier si les alertes mémoire sont activées + */ + public boolean isMemoryAlertEnabled() { + return getConfiguration().getMemoryLowAlertEnabled(); + } + + /** + * Récupérer le seuil CPU + */ + public int getCpuThreshold() { + return getConfiguration().getCpuThresholdPercent(); + } + + /** + * Récupérer le seuil mémoire + */ + public int getMemoryThreshold() { + return getConfiguration().getMemoryThresholdPercent(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/AlerteLcbFtRepository.java b/src/main/java/dev/lions/unionflow/server/repository/AlerteLcbFtRepository.java new file mode 100644 index 0000000..41d05a1 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/AlerteLcbFtRepository.java @@ -0,0 +1,151 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.AlerteLcbFt; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * Repository pour la gestion des alertes LCB-FT. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@ApplicationScoped +public class AlerteLcbFtRepository implements PanacheRepositoryBase { + + /** + * Recherche les alertes avec filtres et pagination + */ + public List search( + UUID organisationId, + String typeAlerte, + Boolean traitee, + LocalDateTime dateDebut, + LocalDateTime dateFin, + int pageIndex, + int pageSize + ) { + StringBuilder jpql = new StringBuilder("SELECT a FROM AlerteLcbFt a WHERE a.actif = true"); + + if (organisationId != null) { + jpql.append(" AND a.organisation.id = :organisationId"); + } + + if (typeAlerte != null && !typeAlerte.isBlank()) { + jpql.append(" AND a.typeAlerte = :typeAlerte"); + } + + if (traitee != null) { + jpql.append(" AND a.traitee = :traitee"); + } + + if (dateDebut != null) { + jpql.append(" AND a.dateAlerte >= :dateDebut"); + } + + if (dateFin != null) { + jpql.append(" AND a.dateAlerte <= :dateFin"); + } + + jpql.append(" ORDER BY a.dateAlerte DESC"); + + var query = getEntityManager().createQuery(jpql.toString(), AlerteLcbFt.class); + + if (organisationId != null) { + query.setParameter("organisationId", organisationId); + } + + if (typeAlerte != null && !typeAlerte.isBlank()) { + query.setParameter("typeAlerte", typeAlerte); + } + + if (traitee != null) { + query.setParameter("traitee", traitee); + } + + if (dateDebut != null) { + query.setParameter("dateDebut", dateDebut); + } + + if (dateFin != null) { + query.setParameter("dateFin", dateFin); + } + + query.setFirstResult(pageIndex * pageSize); + query.setMaxResults(pageSize); + + return query.getResultList(); + } + + /** + * Compte le nombre d'alertes avec filtres + */ + public long count( + UUID organisationId, + String typeAlerte, + Boolean traitee, + LocalDateTime dateDebut, + LocalDateTime dateFin + ) { + StringBuilder jpql = new StringBuilder("SELECT COUNT(a) FROM AlerteLcbFt a WHERE a.actif = true"); + + if (organisationId != null) { + jpql.append(" AND a.organisation.id = :organisationId"); + } + + if (typeAlerte != null && !typeAlerte.isBlank()) { + jpql.append(" AND a.typeAlerte = :typeAlerte"); + } + + if (traitee != null) { + jpql.append(" AND a.traitee = :traitee"); + } + + if (dateDebut != null) { + jpql.append(" AND a.dateAlerte >= :dateDebut"); + } + + if (dateFin != null) { + jpql.append(" AND a.dateAlerte <= :dateFin"); + } + + var query = getEntityManager().createQuery(jpql.toString(), Long.class); + + if (organisationId != null) { + query.setParameter("organisationId", organisationId); + } + + if (typeAlerte != null && !typeAlerte.isBlank()) { + query.setParameter("typeAlerte", typeAlerte); + } + + if (traitee != null) { + query.setParameter("traitee", traitee); + } + + if (dateDebut != null) { + query.setParameter("dateDebut", dateDebut); + } + + if (dateFin != null) { + query.setParameter("dateFin", dateFin); + } + + return query.getSingleResult(); + } + + /** + * Compte les alertes non traitées pour une organisation + */ + public long countNonTraitees(UUID organisationId) { + if (organisationId == null) { + return count("traitee = false and actif = true"); + } + return count("organisation.id = ?1 and traitee = false and actif = true", organisationId); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/SystemAlertRepository.java b/src/main/java/dev/lions/unionflow/server/repository/SystemAlertRepository.java new file mode 100644 index 0000000..7057c67 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/SystemAlertRepository.java @@ -0,0 +1,157 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.SystemAlert; +import io.quarkus.arc.Unremovable; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * Repository pour l'entité SystemAlert + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@ApplicationScoped +@Unremovable +public class SystemAlertRepository extends BaseRepository { + + public SystemAlertRepository() { + super(SystemAlert.class); + } + + /** + * Récupérer toutes les alertes actives (non acquittées) + */ + public List findActiveAlerts() { + TypedQuery query = entityManager.createQuery( + "SELECT a FROM SystemAlert a WHERE a.acknowledged = false ORDER BY a.timestamp DESC", + SystemAlert.class + ); + return query.getResultList(); + } + + /** + * Récupérer toutes les alertes acquittées + */ + public List findAcknowledgedAlerts() { + TypedQuery query = entityManager.createQuery( + "SELECT a FROM SystemAlert a WHERE a.acknowledged = true ORDER BY a.timestamp DESC", + SystemAlert.class + ); + return query.getResultList(); + } + + /** + * Récupérer les alertes par niveau + */ + public List findByLevel(String level) { + TypedQuery query = entityManager.createQuery( + "SELECT a FROM SystemAlert a WHERE a.level = :level ORDER BY a.timestamp DESC", + SystemAlert.class + ); + query.setParameter("level", level); + return query.getResultList(); + } + + /** + * Récupérer les alertes critiques non acquittées + */ + public List findCriticalUnacknowledged() { + TypedQuery query = entityManager.createQuery( + "SELECT a FROM SystemAlert a WHERE a.level = 'CRITICAL' AND a.acknowledged = false ORDER BY a.timestamp DESC", + SystemAlert.class + ); + return query.getResultList(); + } + + /** + * Récupérer les alertes par source + */ + public List findBySource(String source) { + TypedQuery query = entityManager.createQuery( + "SELECT a FROM SystemAlert a WHERE a.source = :source ORDER BY a.timestamp DESC", + SystemAlert.class + ); + query.setParameter("source", source); + return query.getResultList(); + } + + /** + * Acquitter une alerte + */ + public void acknowledgeAlert(UUID alertId, String acknowledgedBy) { + SystemAlert alert = findById(alertId); + if (alert != null) { + alert.setAcknowledged(true); + alert.setAcknowledgedBy(acknowledgedBy); + alert.setAcknowledgedAt(LocalDateTime.now()); + persist(alert); + } + } + + /** + * Compter les alertes actives + */ + public long countActive() { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(a) FROM SystemAlert a WHERE a.acknowledged = false", + Long.class + ); + return query.getSingleResult(); + } + + /** + * Compter les alertes dans les dernières 24h + */ + public long countLast24h() { + LocalDateTime yesterday = LocalDateTime.now().minusHours(24); + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(a) FROM SystemAlert a WHERE a.timestamp >= :yesterday", + Long.class + ); + query.setParameter("yesterday", yesterday); + return query.getSingleResult(); + } + + /** + * Compter les alertes acquittées dans les dernières 24h + */ + public long countAcknowledgedLast24h() { + LocalDateTime yesterday = LocalDateTime.now().minusHours(24); + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(a) FROM SystemAlert a WHERE a.acknowledged = true AND a.timestamp >= :yesterday", + Long.class + ); + query.setParameter("yesterday", yesterday); + return query.getSingleResult(); + } + + /** + * Supprimer les alertes plus anciennes qu'une date donnée (rotation) + */ + public int deleteOlderThan(LocalDateTime threshold) { + return entityManager.createQuery( + "DELETE FROM SystemAlert a WHERE a.timestamp < :threshold" + ) + .setParameter("threshold", threshold) + .executeUpdate(); + } + + /** + * Récupérer les alertes dans une période + */ + public List findByTimestampBetween(LocalDateTime start, LocalDateTime end) { + TypedQuery query = entityManager.createQuery( + "SELECT a FROM SystemAlert a WHERE a.timestamp BETWEEN :start AND :end ORDER BY a.timestamp DESC", + SystemAlert.class + ); + query.setParameter("start", start); + query.setParameter("end", end); + return query.getResultList(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/SystemLogRepository.java b/src/main/java/dev/lions/unionflow/server/repository/SystemLogRepository.java new file mode 100644 index 0000000..8912e7b --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/SystemLogRepository.java @@ -0,0 +1,162 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.SystemLog; +import io.quarkus.arc.Unremovable; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Repository pour l'entité SystemLog + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@ApplicationScoped +@Unremovable +public class SystemLogRepository extends BaseRepository { + + public SystemLogRepository() { + super(SystemLog.class); + } + + /** + * Rechercher les logs par niveau + */ + public List findByLevel(String level) { + TypedQuery query = entityManager.createQuery( + "SELECT l FROM SystemLog l WHERE l.level = :level ORDER BY l.timestamp DESC", + SystemLog.class + ); + query.setParameter("level", level); + return query.getResultList(); + } + + /** + * Rechercher les logs par source + */ + public List findBySource(String source) { + TypedQuery query = entityManager.createQuery( + "SELECT l FROM SystemLog l WHERE l.source = :source ORDER BY l.timestamp DESC", + SystemLog.class + ); + query.setParameter("source", source); + return query.getResultList(); + } + + /** + * Rechercher les logs par niveau et source + */ + public List findByLevelAndSource(String level, String source) { + TypedQuery query = entityManager.createQuery( + "SELECT l FROM SystemLog l WHERE l.level = :level AND l.source = :source ORDER BY l.timestamp DESC", + SystemLog.class + ); + query.setParameter("level", level); + query.setParameter("source", source); + return query.getResultList(); + } + + /** + * Rechercher les logs par période + */ + public List findByTimestampBetween(LocalDateTime start, LocalDateTime end) { + TypedQuery query = entityManager.createQuery( + "SELECT l FROM SystemLog l WHERE l.timestamp BETWEEN :start AND :end ORDER BY l.timestamp DESC", + SystemLog.class + ); + query.setParameter("start", start); + query.setParameter("end", end); + return query.getResultList(); + } + + /** + * Rechercher les logs contenant un texte + */ + public List searchByText(String searchQuery) { + TypedQuery query = entityManager.createQuery( + "SELECT l FROM SystemLog l WHERE LOWER(l.message) LIKE LOWER(:query) OR LOWER(l.source) LIKE LOWER(:query) ORDER BY l.timestamp DESC", + SystemLog.class + ); + query.setParameter("query", "%" + searchQuery + "%"); + return query.getResultList(); + } + + /** + * Recherche avancée avec tous les filtres + */ + public List search(String level, String source, String searchQuery, LocalDateTime start, LocalDateTime end, int pageIndex, int pageSize) { + StringBuilder jpql = new StringBuilder("SELECT l FROM SystemLog l WHERE 1=1"); + + if (level != null && !level.isBlank() && !"TOUS".equals(level)) { + jpql.append(" AND l.level = :level"); + } + if (source != null && !source.isBlank() && !"TOUS".equals(source)) { + jpql.append(" AND l.source = :source"); + } + if (searchQuery != null && !searchQuery.isBlank()) { + jpql.append(" AND (LOWER(l.message) LIKE LOWER(:query) OR LOWER(l.source) LIKE LOWER(:query))"); + } + if (start != null) { + jpql.append(" AND l.timestamp >= :start"); + } + if (end != null) { + jpql.append(" AND l.timestamp <= :end"); + } + + jpql.append(" ORDER BY l.timestamp DESC"); + + TypedQuery query = entityManager.createQuery(jpql.toString(), SystemLog.class); + + if (level != null && !level.isBlank() && !"TOUS".equals(level)) { + query.setParameter("level", level); + } + if (source != null && !source.isBlank() && !"TOUS".equals(source)) { + query.setParameter("source", source); + } + if (searchQuery != null && !searchQuery.isBlank()) { + query.setParameter("query", "%" + searchQuery + "%"); + } + if (start != null) { + query.setParameter("start", start); + } + if (end != null) { + query.setParameter("end", end); + } + + query.setFirstResult(pageIndex * pageSize); + query.setMaxResults(pageSize); + + return query.getResultList(); + } + + /** + * Compter les logs par niveau dans les dernières 24h + */ + public long countByLevelLast24h(String level) { + LocalDateTime yesterday = LocalDateTime.now().minusHours(24); + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(l) FROM SystemLog l WHERE l.level = :level AND l.timestamp >= :yesterday", + Long.class + ); + query.setParameter("level", level); + query.setParameter("yesterday", yesterday); + return query.getSingleResult(); + } + + /** + * Supprimer les logs plus anciens qu'une date donnée (rotation) + */ + public int deleteOlderThan(LocalDateTime threshold) { + return entityManager.createQuery( + "DELETE FROM SystemLog l WHERE l.timestamp < :threshold" + ) + .setParameter("threshold", threshold) + .executeUpdate(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/AlerteLcbFtResource.java b/src/main/java/dev/lions/unionflow/server/resource/AlerteLcbFtResource.java new file mode 100644 index 0000000..f5d38ad --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/AlerteLcbFtResource.java @@ -0,0 +1,161 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.lcbft.AlerteLcbFtResponse; +import dev.lions.unionflow.server.entity.AlerteLcbFt; +import dev.lions.unionflow.server.repository.AlerteLcbFtRepository; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * API REST pour la gestion des alertes LCB-FT. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@Path("/api/alertes-lcb-ft") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Alertes LCB-FT", description = "Gestion des alertes Lutte Contre le Blanchiment") +public class AlerteLcbFtResource { + + @Inject + AlerteLcbFtRepository alerteLcbFtRepository; + + /** + * Récupère les alertes LCB-FT avec filtres et pagination. + */ + @GET + @RolesAllowed({"ORG_ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Liste des alertes LCB-FT", description = "Récupère les alertes avec filtrage et pagination") + public Response getAlertes( + @QueryParam("organisationId") String organisationId, + @QueryParam("typeAlerte") String typeAlerte, + @QueryParam("traitee") Boolean traitee, + @QueryParam("dateDebut") String dateDebut, + @QueryParam("dateFin") String dateFin, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size + ) { + UUID orgId = organisationId != null && !organisationId.isBlank() ? UUID.fromString(organisationId) : null; + LocalDateTime debut = dateDebut != null && !dateDebut.isBlank() ? LocalDateTime.parse(dateDebut) : null; + LocalDateTime fin = dateFin != null && !dateFin.isBlank() ? LocalDateTime.parse(dateFin) : null; + + List alertes = alerteLcbFtRepository.search( + orgId, + typeAlerte, + traitee, + debut, + fin, + page, + size + ); + + long total = alerteLcbFtRepository.count(orgId, typeAlerte, traitee, debut, fin); + + List responses = alertes.stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + + Map result = new HashMap<>(); + result.put("content", responses); + result.put("totalElements", total); + result.put("totalPages", (int) Math.ceil((double) total / size)); + result.put("currentPage", page); + result.put("pageSize", size); + + return Response.ok(result).build(); + } + + /** + * Récupère une alerte par son ID. + */ + @GET + @Path("/{id}") + @RolesAllowed({"ORG_ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Détails d'une alerte", description = "Récupère une alerte par son ID") + public Response getAlerteById(@PathParam("id") String id) { + AlerteLcbFt alerte = alerteLcbFtRepository.findById(UUID.fromString(id)); + if (alerte == null) { + throw new NotFoundException("Alerte non trouvée"); + } + + return Response.ok(mapToResponse(alerte)).build(); + } + + /** + * Marque une alerte comme traitée. + */ + @POST + @Path("/{id}/traiter") + @RolesAllowed({"ORG_ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Traiter une alerte", description = "Marque une alerte comme traitée avec un commentaire") + public Response traiterAlerte( + @PathParam("id") String id, + Map body + ) { + AlerteLcbFt alerte = alerteLcbFtRepository.findById(UUID.fromString(id)); + if (alerte == null) { + throw new NotFoundException("Alerte non trouvée"); + } + + alerte.setTraitee(true); + alerte.setDateTraitement(LocalDateTime.now()); + alerte.setTraitePar(body.get("traitePar")); + alerte.setCommentaireTraitement(body.get("commentaire")); + + alerteLcbFtRepository.persist(alerte); + + return Response.ok(mapToResponse(alerte)).build(); + } + + /** + * Compte les alertes non traitées. + */ + @GET + @Path("/stats/non-traitees") + @RolesAllowed({"ORG_ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Statistiques alertes", description = "Nombre d'alertes non traitées") + public Response getStatsNonTraitees(@QueryParam("organisationId") String organisationId) { + UUID orgId = organisationId != null && !organisationId.isBlank() ? UUID.fromString(organisationId) : null; + long count = alerteLcbFtRepository.countNonTraitees(orgId); + + return Response.ok(Map.of("count", count)).build(); + } + + private AlerteLcbFtResponse mapToResponse(AlerteLcbFt alerte) { + return AlerteLcbFtResponse.builder() + .id(alerte.getId().toString()) + .organisationId(alerte.getOrganisation() != null ? alerte.getOrganisation().getId().toString() : null) + .organisationNom(alerte.getOrganisation() != null ? alerte.getOrganisation().getNom() : null) + .membreId(alerte.getMembre() != null ? alerte.getMembre().getId().toString() : null) + .membreNomComplet(alerte.getMembre() != null ? + alerte.getMembre().getPrenom() + " " + alerte.getMembre().getNom() : null) + .typeAlerte(alerte.getTypeAlerte()) + .dateAlerte(alerte.getDateAlerte()) + .description(alerte.getDescription()) + .details(alerte.getDetails()) + .montant(alerte.getMontant()) + .seuil(alerte.getSeuil()) + .typeOperation(alerte.getTypeOperation()) + .transactionRef(alerte.getTransactionRef()) + .severite(alerte.getSeverite()) + .traitee(alerte.getTraitee()) + .dateTraitement(alerte.getDateTraitement()) + .traitePar(alerte.getTraitePar()) + .commentaireTraitement(alerte.getCommentaireTraitement()) + .build(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/DocumentResource.java b/src/main/java/dev/lions/unionflow/server/resource/DocumentResource.java index 63cd53d..c8dd59b 100644 --- a/src/main/java/dev/lions/unionflow/server/resource/DocumentResource.java +++ b/src/main/java/dev/lions/unionflow/server/resource/DocumentResource.java @@ -5,12 +5,18 @@ import dev.lions.unionflow.server.api.dto.document.response.DocumentResponse; import dev.lions.unionflow.server.api.dto.document.request.CreatePieceJointeRequest; import dev.lions.unionflow.server.api.dto.document.response.PieceJointeResponse; import dev.lions.unionflow.server.service.DocumentService; +import dev.lions.unionflow.server.service.FileStorageService; +import dev.lions.unionflow.server.api.enums.document.TypeDocument; +import dev.lions.unionflow.server.entity.Document; +import dev.lions.unionflow.server.repository.DocumentRepository; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; +import org.jboss.resteasy.reactive.multipart.FileUpload; import jakarta.validation.Valid; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.nio.file.Files; import java.util.List; import java.util.UUID; import org.eclipse.microprofile.openapi.annotations.tags.Tag; @@ -35,6 +41,12 @@ public class DocumentResource { @Inject DocumentService documentService; + @Inject + FileStorageService fileStorageService; + + @Inject + DocumentRepository documentRepository; + /** * Crée un nouveau document * @@ -55,6 +67,84 @@ public class DocumentResource { } } + /** + * Upload un fichier (image ou PDF) pour justificatif LCB-FT + * + * @param file Fichier uploadé + * @param description Description optionnelle + * @return ID du document créé + */ + @POST + @Path("/upload") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @RolesAllowed({ "ADMIN", "MEMBRE" }) + @jakarta.transaction.Transactional + public Response uploadFile( + @org.jboss.resteasy.reactive.RestForm("file") FileUpload file, + @org.jboss.resteasy.reactive.RestForm("description") String description, + @org.jboss.resteasy.reactive.RestForm("typeDocument") String typeDocument + ) { + try { + if (file == null || file.fileName() == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Aucun fichier fourni")) + .build(); + } + + LOG.infof("Upload de fichier: %s (%d octets, type: %s)", + file.fileName(), file.size(), file.contentType()); + + // Stocker le fichier physiquement + FileStorageService.FileMetadata metadata; + try (var inputStream = Files.newInputStream(file.filePath())) { + metadata = fileStorageService.storeFile( + inputStream, + file.fileName(), + file.contentType(), + file.size() + ); + } + + // Créer l'entité Document en BDD + Document document = Document.builder() + .nomFichier(metadata.getNomFichier()) + .nomOriginal(metadata.getNomOriginal()) + .cheminStockage(metadata.getCheminStockage()) + .typeMime(metadata.getTypeMime()) + .tailleOctets(metadata.getTailleOctets()) + .hashMd5(metadata.getHashMd5()) + .hashSha256(metadata.getHashSha256()) + .description(description) + .typeDocument(typeDocument != null ? TypeDocument.valueOf(typeDocument) : TypeDocument.PIECE_JUSTIFICATIVE) + .build(); + + documentRepository.persist(document); + + LOG.infof("Document créé avec ID: %s", document.getId()); + + // Retourner l'ID du document (pour référencer dans TransactionEpargneRequest) + return Response.status(Response.Status.CREATED) + .entity(java.util.Map.of( + "id", document.getId().toString(), + "nomFichier", document.getNomFichier(), + "taille", document.getTailleFormatee(), + "typeMime", document.getTypeMime() + )) + .build(); + + } catch (IllegalArgumentException e) { + LOG.warnf("Validation échouée pour upload: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'upload du fichier"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse("Erreur lors de l'upload: " + e.getMessage())) + .build(); + } + } + /** * Trouve un document par son ID * diff --git a/src/main/java/dev/lions/unionflow/server/resource/SystemResource.java b/src/main/java/dev/lions/unionflow/server/resource/SystemResource.java index 22ab917..f21d6c9 100644 --- a/src/main/java/dev/lions/unionflow/server/resource/SystemResource.java +++ b/src/main/java/dev/lions/unionflow/server/resource/SystemResource.java @@ -3,7 +3,7 @@ package dev.lions.unionflow.server.resource; import dev.lions.unionflow.server.api.dto.system.request.UpdateSystemConfigRequest; import dev.lions.unionflow.server.api.dto.system.response.CacheStatsResponse; import dev.lions.unionflow.server.api.dto.system.response.SystemConfigResponse; -import dev.lions.unionflow.server.api.dto.system.response.SystemMetricsResponse; +import dev.lions.unionflow.server.api.dto.logs.response.SystemMetricsResponse; import dev.lions.unionflow.server.api.dto.system.response.SystemTestResultResponse; import dev.lions.unionflow.server.service.SystemConfigService; import dev.lions.unionflow.server.service.SystemMetricsService; diff --git a/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java b/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java new file mode 100644 index 0000000..85179ea --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/AlertMonitoringService.java @@ -0,0 +1,249 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.entity.AlertConfiguration; +import dev.lions.unionflow.server.entity.SystemAlert; +import dev.lions.unionflow.server.repository.AlertConfigurationRepository; +import dev.lions.unionflow.server.repository.SystemAlertRepository; +import dev.lions.unionflow.server.repository.SystemLogRepository; +import io.quarkus.scheduler.Scheduled; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.OperatingSystemMXBean; +import java.time.LocalDateTime; + +/** + * Service de monitoring automatique qui vérifie les métriques système + * et génère des alertes automatiquement selon la configuration. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@Slf4j +@ApplicationScoped +public class AlertMonitoringService { + + @Inject + AlertConfigurationRepository alertConfigurationRepository; + + @Inject + SystemAlertRepository systemAlertRepository; + + @Inject + SystemLogRepository systemLogRepository; + + // Stocker la dernière valeur CPU pour détecter les dépassements prolongés + private Double lastCpuUsage = 0.0; + private LocalDateTime lastCpuHighTime = null; + + /** + * Scheduler qui s'exécute toutes les minutes pour vérifier les métriques + */ + @Scheduled(cron = "0 * * * * ?") // Toutes les minutes à la seconde 0 + @Transactional + public void monitorSystemMetrics() { + try { + log.debug("Running scheduled system metrics monitoring..."); + + AlertConfiguration config = alertConfigurationRepository.getConfiguration(); + + // Vérifier CPU + if (config.getCpuHighAlertEnabled()) { + checkCpuThreshold(config); + } + + // Vérifier mémoire + if (config.getMemoryLowAlertEnabled()) { + checkMemoryThreshold(config); + } + + // Vérifier erreurs critiques + if (config.getCriticalErrorAlertEnabled()) { + checkCriticalErrors(); + } + + } catch (Exception e) { + log.error("Error in scheduled system monitoring", e); + } + } + + /** + * Vérifier si le CPU dépasse le seuil configuré + */ + private void checkCpuThreshold(AlertConfiguration config) { + try { + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + double loadAvg = osBean.getSystemLoadAverage(); + int processors = osBean.getAvailableProcessors(); + + // Calculer l'utilisation CPU en pourcentage + double cpuUsage = loadAvg < 0 ? 0.0 : Math.min(100.0, (loadAvg / processors) * 100.0); + lastCpuUsage = cpuUsage; + + int threshold = config.getCpuThresholdPercent(); + int durationMinutes = config.getCpuDurationMinutes(); + + // Vérifier si le seuil est dépassé + if (cpuUsage > threshold) { + if (lastCpuHighTime == null) { + lastCpuHighTime = LocalDateTime.now(); + } else { + // Vérifier si le dépassement dure depuis assez longtemps + LocalDateTime now = LocalDateTime.now(); + long minutesSinceHigh = java.time.Duration.between(lastCpuHighTime, now).toMinutes(); + + if (minutesSinceHigh >= durationMinutes) { + // Créer une alerte seulement si pas déjà créée récemment + if (!hasRecentCpuAlert()) { + createCpuAlert(cpuUsage, threshold); + log.warn("CPU alert created: {}% (threshold: {}%)", cpuUsage, threshold); + } + } + } + } else { + // Reset le compteur si CPU revient sous le seuil + lastCpuHighTime = null; + } + + } catch (Exception e) { + log.error("Error checking CPU threshold", e); + } + } + + /** + * Vérifier si la mémoire dépasse le seuil configuré + */ + private void checkMemoryThreshold(AlertConfiguration config) { + try { + MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); + long maxMemory = memoryBean.getHeapMemoryUsage().getMax(); + long usedMemory = memoryBean.getHeapMemoryUsage().getUsed(); + + double memoryUsage = maxMemory > 0 ? (usedMemory * 100.0 / maxMemory) : 0.0; + int threshold = config.getMemoryThresholdPercent(); + + if (memoryUsage > threshold) { + // Créer une alerte seulement si pas déjà créée récemment + if (!hasRecentMemoryAlert()) { + createMemoryAlert(memoryUsage, threshold); + log.warn("Memory alert created: {}% (threshold: {}%)", memoryUsage, threshold); + } + } + + } catch (Exception e) { + log.error("Error checking memory threshold", e); + } + } + + /** + * Vérifier le nombre d'erreurs critiques dans la dernière heure + */ + private void checkCriticalErrors() { + try { + long criticalCount = systemLogRepository.countByLevelLast24h("CRITICAL"); + + // Si plus de 5 erreurs critiques dans les dernières 24h, créer une alerte + if (criticalCount > 5) { + if (!hasRecentCriticalErrorsAlert()) { + createCriticalErrorsAlert(criticalCount); + log.warn("Critical errors alert created: {} errors in last 24h", criticalCount); + } + } + + } catch (Exception e) { + log.error("Error checking critical errors", e); + } + } + + /** + * Créer une alerte CPU + */ + private void createCpuAlert(double currentValue, int threshold) { + SystemAlert alert = new SystemAlert(); + alert.setLevel("WARNING"); + alert.setTitle("Utilisation CPU élevée"); + alert.setMessage(String.format("L'utilisation CPU dépasse le seuil configuré")); + alert.setTimestamp(LocalDateTime.now()); + alert.setAcknowledged(false); + alert.setSource("CPU"); + alert.setAlertType("THRESHOLD"); + alert.setCurrentValue(currentValue); + alert.setThresholdValue((double) threshold); + alert.setUnit("%"); + alert.setRecommendedActions("Vérifier les processus en cours d'exécution. Redémarrer les services si nécessaire."); + + systemAlertRepository.persist(alert); + } + + /** + * Créer une alerte mémoire + */ + private void createMemoryAlert(double currentValue, int threshold) { + SystemAlert alert = new SystemAlert(); + alert.setLevel("WARNING"); + alert.setTitle("Utilisation mémoire élevée"); + alert.setMessage(String.format("L'utilisation mémoire dépasse le seuil configuré")); + alert.setTimestamp(LocalDateTime.now()); + alert.setAcknowledged(false); + alert.setSource("MEMORY"); + alert.setAlertType("THRESHOLD"); + alert.setCurrentValue(currentValue); + alert.setThresholdValue((double) threshold); + alert.setUnit("%"); + alert.setRecommendedActions("Vérifier les applications consommant de la mémoire. Considérer l'augmentation des ressources."); + + systemAlertRepository.persist(alert); + } + + /** + * Créer une alerte pour erreurs critiques + */ + private void createCriticalErrorsAlert(long errorCount) { + SystemAlert alert = new SystemAlert(); + alert.setLevel("CRITICAL"); + alert.setTitle("Erreurs critiques détectées"); + alert.setMessage(String.format("%d erreurs critiques détectées dans les dernières 24h", errorCount)); + alert.setTimestamp(LocalDateTime.now()); + alert.setAcknowledged(false); + alert.setSource("System"); + alert.setAlertType("ERROR"); + alert.setCurrentValue((double) errorCount); + alert.setThresholdValue(5.0); + alert.setUnit("erreurs"); + alert.setRecommendedActions("Consulter les logs système pour identifier la cause. Corriger les erreurs critiques."); + + systemAlertRepository.persist(alert); + } + + /** + * Vérifier si une alerte CPU a été créée récemment (dans les 30 dernières minutes) + */ + private boolean hasRecentCpuAlert() { + LocalDateTime thirtyMinutesAgo = LocalDateTime.now().minusMinutes(30); + return systemAlertRepository.findByTimestampBetween(thirtyMinutesAgo, LocalDateTime.now()).stream() + .anyMatch(alert -> "CPU".equals(alert.getSource()) && !alert.getAcknowledged()); + } + + /** + * Vérifier si une alerte mémoire a été créée récemment (dans les 30 dernières minutes) + */ + private boolean hasRecentMemoryAlert() { + LocalDateTime thirtyMinutesAgo = LocalDateTime.now().minusMinutes(30); + return systemAlertRepository.findByTimestampBetween(thirtyMinutesAgo, LocalDateTime.now()).stream() + .anyMatch(alert -> "MEMORY".equals(alert.getSource()) && !alert.getAcknowledged()); + } + + /** + * Vérifier si une alerte erreurs critiques a été créée récemment (dans la dernière heure) + */ + private boolean hasRecentCriticalErrorsAlert() { + LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1); + return systemAlertRepository.findByTimestampBetween(oneHourAgo, LocalDateTime.now()).stream() + .anyMatch(alert -> "System".equals(alert.getSource()) && "ERROR".equals(alert.getAlertType()) && !alert.getAcknowledged()); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/AlerteLcbFtService.java b/src/main/java/dev/lions/unionflow/server/service/AlerteLcbFtService.java new file mode 100644 index 0000000..4cb2598 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/AlerteLcbFtService.java @@ -0,0 +1,162 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.entity.AlerteLcbFt; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.AlerteLcbFtRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * Service pour la génération automatique d'alertes LCB-FT. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@Slf4j +@ApplicationScoped +public class AlerteLcbFtService { + + @Inject + AlerteLcbFtRepository alerteLcbFtRepository; + + @Inject + OrganisationRepository organisationRepository; + + @Inject + MembreRepository membreRepository; + + /** + * Génère une alerte lorsqu'un seuil LCB-FT est dépassé. + */ + @Transactional + public void genererAlerteSeuilDepasse( + UUID organisationId, + UUID membreId, + String typeOperation, + BigDecimal montant, + BigDecimal seuil, + String transactionRef, + String origineFonds + ) { + try { + Organisation organisation = organisationRepository.findByIdOptional(organisationId) + .orElse(null); + + Membre membre = membreId != null ? membreRepository.findByIdOptional(membreId).orElse(null) : null; + + String description = String.format( + "Transaction %s de %s FCFA dépassant le seuil LCB-FT de %s FCFA", + typeOperation != null ? typeOperation : "inconnue", + montant != null ? montant.toPlainString() : "0", + seuil != null ? seuil.toPlainString() : "0" + ); + + String details = String.format( + "Référence: %s | Origine des fonds: %s | Écart: %s FCFA", + transactionRef != null ? transactionRef : "N/A", + origineFonds != null && !origineFonds.isBlank() ? origineFonds : "NON FOURNI", + montant != null && seuil != null ? montant.subtract(seuil).toPlainString() : "N/A" + ); + + // Déterminer la sévérité selon l'écart avec le seuil + String severite = "INFO"; + if (montant != null && seuil != null) { + BigDecimal ecart = montant.subtract(seuil); + BigDecimal ratio = seuil.compareTo(BigDecimal.ZERO) > 0 ? + ecart.divide(seuil, 2, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO; + + if (ratio.compareTo(new BigDecimal("2.0")) >= 0) { // > 200% du seuil + severite = "CRITICAL"; + } else if (ratio.compareTo(new BigDecimal("0.5")) >= 0) { // > 50% du seuil + severite = "WARNING"; + } + } + + // Vérifier si origine des fonds est fournie + if (origineFonds == null || origineFonds.isBlank()) { + details += " | ⚠️ JUSTIFICATION MANQUANTE"; + severite = "WARNING"; // Toujours WARNING minimum si pas de justification + } + + AlerteLcbFt alerte = AlerteLcbFt.builder() + .organisation(organisation) + .membre(membre) + .typeAlerte("SEUIL_DEPASSE") + .dateAlerte(LocalDateTime.now()) + .description(description) + .details(details) + .montant(montant) + .seuil(seuil) + .typeOperation(typeOperation) + .transactionRef(transactionRef) + .severite(severite) + .traitee(false) + .build(); + + alerteLcbFtRepository.persist(alerte); + + log.info("Alerte LCB-FT générée : {} - Sévérité: {} - Montant: {} FCFA", + alerte.getId(), severite, montant); + + } catch (Exception e) { + // Ne pas bloquer la transaction si la génération d'alerte échoue + log.error("Erreur lors de la génération d'alerte LCB-FT", e); + } + } + + /** + * Génère une alerte pour justification manquante. + */ + @Transactional + public void genererAlerteJustificationManquante( + UUID organisationId, + UUID membreId, + String typeOperation, + BigDecimal montant, + String transactionRef + ) { + try { + Organisation organisation = organisationRepository.findByIdOptional(organisationId) + .orElse(null); + + Membre membre = membreId != null ? membreRepository.findByIdOptional(membreId).orElse(null) : null; + + String description = String.format( + "Origine des fonds non fournie pour %s de %s FCFA", + typeOperation != null ? typeOperation : "transaction", + montant != null ? montant.toPlainString() : "0" + ); + + AlerteLcbFt alerte = AlerteLcbFt.builder() + .organisation(organisation) + .membre(membre) + .typeAlerte("JUSTIFICATION_MANQUANTE") + .dateAlerte(LocalDateTime.now()) + .description(description) + .details("Référence: " + (transactionRef != null ? transactionRef : "N/A")) + .montant(montant) + .typeOperation(typeOperation) + .transactionRef(transactionRef) + .severite("WARNING") + .traitee(false) + .build(); + + alerteLcbFtRepository.persist(alerte); + + log.warn("Alerte justification manquante générée pour transaction: {}", transactionRef); + + } catch (Exception e) { + log.error("Erreur lors de la génération d'alerte justification manquante", e); + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/FileStorageService.java b/src/main/java/dev/lions/unionflow/server/service/FileStorageService.java new file mode 100644 index 0000000..f649f5c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/FileStorageService.java @@ -0,0 +1,195 @@ +package dev.lions.unionflow.server.service; + +import jakarta.enterprise.context.ApplicationScoped; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * Service de stockage de fichiers sur le système de fichiers. + * Gère l'upload, le stockage, et le calcul des hash. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@Slf4j +@ApplicationScoped +public class FileStorageService { + + // Taille max: 5 MB + private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; + + // Types MIME autorisés + private static final String[] ALLOWED_MIME_TYPES = { + "image/jpeg", + "image/jpg", + "image/png", + "image/gif", + "application/pdf" + }; + + @ConfigProperty(name = "unionflow.upload.directory", defaultValue = "./uploads") + String uploadDirectory; + + /** + * Stocke un fichier uploadé et retourne les métadonnées. + * + * @param inputStream Flux du fichier + * @param fileName Nom du fichier original + * @param mimeType Type MIME + * @param fileSize Taille du fichier + * @return Métadonnées du fichier stocké + * @throws IOException Si erreur d'I/O + * @throws IllegalArgumentException Si validation échoue + */ + public FileMetadata storeFile(InputStream inputStream, String fileName, String mimeType, long fileSize) + throws IOException { + + // Validation de la taille + if (fileSize > MAX_FILE_SIZE) { + throw new IllegalArgumentException( + String.format("Fichier trop volumineux. Taille max: %d MB", MAX_FILE_SIZE / (1024 * 1024)) + ); + } + + // Validation du type MIME + if (!isAllowedMimeType(mimeType)) { + throw new IllegalArgumentException( + "Type de fichier non autorisé. Types acceptés: JPEG, PNG, GIF, PDF" + ); + } + + // Générer un nom unique pour le fichier + String uniqueFileName = generateUniqueFileName(fileName); + + // Créer le chemin de stockage (organisé par date: uploads/2026/03/15/) + String relativePath = getRelativePath(uniqueFileName); + Path targetPath = Paths.get(uploadDirectory, relativePath); + + // Créer les répertoires si nécessaire + Files.createDirectories(targetPath.getParent()); + + // Écrire le fichier et calculer les hash simultanément + MessageDigest md5 = null; + MessageDigest sha256 = null; + try { + md5 = MessageDigest.getInstance("MD5"); + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (Exception e) { + log.warn("Impossible de créer les MessageDigest pour hash: {}", e.getMessage()); + } + + byte[] buffer = new byte[8192]; + int bytesRead; + long totalBytes = 0; + + try (FileOutputStream fos = new FileOutputStream(targetPath.toFile())) { + while ((bytesRead = inputStream.read(buffer)) != -1) { + fos.write(buffer, 0, bytesRead); + totalBytes += bytesRead; + + // Calculer les hash + if (md5 != null) { + md5.update(buffer, 0, bytesRead); + } + if (sha256 != null) { + sha256.update(buffer, 0, bytesRead); + } + } + } + + // Générer les hash + String hashMd5 = md5 != null ? bytesToHex(md5.digest()) : null; + String hashSha256 = sha256 != null ? bytesToHex(sha256.digest()) : null; + + log.info("Fichier stocké : {} ({} octets) - MD5: {}", uniqueFileName, totalBytes, hashMd5); + + return FileMetadata.builder() + .nomFichier(uniqueFileName) + .nomOriginal(fileName) + .cheminStockage(relativePath) + .typeMime(mimeType) + .tailleOctets(totalBytes) + .hashMd5(hashMd5) + .hashSha256(hashSha256) + .build(); + } + + /** + * Vérifie si le type MIME est autorisé + */ + private boolean isAllowedMimeType(String mimeType) { + if (mimeType == null) { + return false; + } + for (String allowed : ALLOWED_MIME_TYPES) { + if (allowed.equalsIgnoreCase(mimeType)) { + return true; + } + } + return false; + } + + /** + * Génère un nom unique pour le fichier + */ + private String generateUniqueFileName(String originalFileName) { + String extension = ""; + int lastDot = originalFileName.lastIndexOf('.'); + if (lastDot > 0) { + extension = originalFileName.substring(lastDot); + } + return UUID.randomUUID().toString() + extension; + } + + /** + * Retourne le chemin relatif organisé par date (YYYY/MM/DD/) + */ + private String getRelativePath(String fileName) { + LocalDate today = LocalDate.now(); + return String.format("%04d/%02d/%02d/%s", + today.getYear(), + today.getMonthValue(), + today.getDayOfMonth(), + fileName + ); + } + + /** + * Convertit un tableau de bytes en hexadécimal + */ + private String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + /** + * Classe pour encapsuler les métadonnées d'un fichier stocké + */ + @lombok.Data + @lombok.Builder + public static class FileMetadata { + private String nomFichier; + private String nomOriginal; + private String cheminStockage; + private String typeMime; + private Long tailleOctets; + private String hashMd5; + private String hashSha256; + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/LogsMonitoringService.java b/src/main/java/dev/lions/unionflow/server/service/LogsMonitoringService.java index f60d7ab..e9de12f 100644 --- a/src/main/java/dev/lions/unionflow/server/service/LogsMonitoringService.java +++ b/src/main/java/dev/lions/unionflow/server/service/LogsMonitoringService.java @@ -6,7 +6,15 @@ import dev.lions.unionflow.server.api.dto.logs.response.AlertConfigResponse; import dev.lions.unionflow.server.api.dto.logs.response.SystemAlertResponse; import dev.lions.unionflow.server.api.dto.logs.response.SystemLogResponse; import dev.lions.unionflow.server.api.dto.logs.response.SystemMetricsResponse; +import dev.lions.unionflow.server.entity.AlertConfiguration; +import dev.lions.unionflow.server.entity.SystemAlert; +import dev.lions.unionflow.server.entity.SystemLog; +import dev.lions.unionflow.server.repository.AlertConfigurationRepository; +import dev.lions.unionflow.server.repository.SystemAlertRepository; +import dev.lions.unionflow.server.repository.SystemLogRepository; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import lombok.extern.slf4j.Slf4j; import java.lang.management.ManagementFactory; @@ -27,6 +35,15 @@ public class LogsMonitoringService { private final LocalDateTime systemStartTime = LocalDateTime.now(); + @Inject + SystemLogRepository systemLogRepository; + + @Inject + SystemAlertRepository systemAlertRepository; + + @Inject + AlertConfigurationRepository alertConfigurationRepository; + /** * Rechercher dans les logs système */ @@ -34,34 +51,41 @@ public class LogsMonitoringService { log.debug("Recherche de logs avec filtres: level={}, source={}, query={}", request.getLevel(), request.getSource(), request.getSearchQuery()); - // Dans une vraie implémentation, on interrogerait une DB ou un système de logs - // Pour l'instant, on retourne des données de test - List allLogs = generateMockLogs(); + // Calculer les dates de début et fin selon timeRange + LocalDateTime start = null; + LocalDateTime end = LocalDateTime.now(); - // Filtrage par niveau - if (request.getLevel() != null && !"TOUS".equals(request.getLevel())) { - allLogs = allLogs.stream() - .filter(log -> log.getLevel().equals(request.getLevel())) - .collect(Collectors.toList()); + if (request.getTimeRange() != null) { + switch (request.getTimeRange()) { + case "1H" -> start = end.minusHours(1); + case "24H" -> start = end.minusHours(24); + case "7D" -> start = end.minusDays(7); + case "30D" -> start = end.minusDays(30); + } } - // Filtrage par source - if (request.getSource() != null && !"TOUS".equals(request.getSource())) { - allLogs = allLogs.stream() - .filter(log -> log.getSource().equals(request.getSource())) - .collect(Collectors.toList()); - } + // Rechercher dans la BDD + int offset = request.getOffset() != null ? request.getOffset() : 0; + int limit = request.getLimit() != null ? request.getLimit() : 100; - // Filtrage par recherche textuelle - if (request.getSearchQuery() != null && !request.getSearchQuery().isBlank()) { - String query = request.getSearchQuery().toLowerCase(); - allLogs = allLogs.stream() - .filter(log -> log.getMessage().toLowerCase().contains(query) - || log.getSource().toLowerCase().contains(query)) - .collect(Collectors.toList()); - } + // Convertir offset/limit en page/pageSize + int pageSize = limit; + int pageIndex = offset / pageSize; - return allLogs; + List logs = systemLogRepository.search( + request.getLevel(), + request.getSource(), + request.getSearchQuery(), + start, + end, + pageIndex, + pageSize + ); + + // Mapper vers DTO + return logs.stream() + .map(this::mapToLogResponse) + .collect(Collectors.toList()); } /** @@ -127,7 +151,7 @@ public class LogsMonitoringService { .networkUsageMbps(12.3 + ThreadLocalRandom.current().nextDouble(5)) .activeConnections(1200 + ThreadLocalRandom.current().nextInt(100)) .errorRate(0.02) - .averageResponseTimeMs(127L) + .averageResponseTimeMs(127.0) .uptime(uptimeMs) .uptimeFormatted(uptimeFormatted) .services(services) @@ -145,48 +169,26 @@ public class LogsMonitoringService { public List getActiveAlerts() { log.debug("Récupération des alertes actives"); - // Dans une vraie implémentation, on interrogerait la DB - List alerts = new ArrayList<>(); + List alerts = systemAlertRepository.findActiveAlerts(); - alerts.add(SystemAlertResponse.builder() - .id(UUID.randomUUID()) - .level("WARNING") - .title("CPU élevé") - .message("Utilisation CPU > 80% pendant 5 minutes") - .timestamp(LocalDateTime.now().minusMinutes(12)) - .acknowledged(false) - .source("CPU") - .alertType("THRESHOLD") - .currentValue(85.5) - .thresholdValue(80.0) - .build()); - - alerts.add(SystemAlertResponse.builder() - .id(UUID.randomUUID()) - .level("INFO") - .title("Sauvegarde terminée") - .message("Sauvegarde automatique réussie (2.3 GB)") - .timestamp(LocalDateTime.now().minusHours(2)) - .acknowledged(true) - .acknowledgedBy("admin@unionflow.test") - .acknowledgedAt(LocalDateTime.now().minusHours(1).minusMinutes(50)) - .source("BACKUP") - .alertType("INFO") - .build()); - - return alerts; + return alerts.stream() + .map(this::mapToAlertResponse) + .collect(Collectors.toList()); } /** * Acquitter une alerte */ + @Transactional public void acknowledgeAlert(UUID alertId) { log.info("Acquittement de l'alerte: {}", alertId); - // Dans une vraie implémentation, on mettrait à jour en DB - // TODO: Marquer l'alerte comme acquittée en DB + // TODO: Récupérer l'utilisateur courant depuis le contexte de sécurité + String currentUser = "admin@unionflow.test"; // Temporaire - log.info("Alerte acquittée avec succès"); + systemAlertRepository.acknowledgeAlert(alertId, currentUser); + + log.info("Alerte {} acquittée avec succès par {}", alertId, currentUser); } /** @@ -195,137 +197,131 @@ public class LogsMonitoringService { public AlertConfigResponse getAlertConfig() { log.debug("Récupération de la configuration des alertes"); + AlertConfiguration config = alertConfigurationRepository.getConfiguration(); + + // Compter les alertes réelles + long totalLast24h = systemAlertRepository.countLast24h(); + long activeAlerts = systemAlertRepository.countActive(); + long acknowledgedLast24h = systemAlertRepository.countAcknowledgedLast24h(); + return AlertConfigResponse.builder() - .cpuHighAlertEnabled(true) - .cpuThresholdPercent(80) - .cpuDurationMinutes(5) - .memoryLowAlertEnabled(true) - .memoryThresholdPercent(85) - .criticalErrorAlertEnabled(true) - .errorAlertEnabled(true) - .connectionFailureAlertEnabled(true) - .connectionFailureThreshold(100) - .connectionFailureWindowMinutes(5) - .emailNotificationsEnabled(true) - .pushNotificationsEnabled(false) - .smsNotificationsEnabled(false) - .alertEmailRecipients("admin@unionflow.test,support@unionflow.test") - .totalAlertsLast24h(15) - .activeAlerts(2) - .acknowledgedAlerts(13) + .cpuHighAlertEnabled(config.getCpuHighAlertEnabled()) + .cpuThresholdPercent(config.getCpuThresholdPercent()) + .cpuDurationMinutes(config.getCpuDurationMinutes()) + .memoryLowAlertEnabled(config.getMemoryLowAlertEnabled()) + .memoryThresholdPercent(config.getMemoryThresholdPercent()) + .criticalErrorAlertEnabled(config.getCriticalErrorAlertEnabled()) + .errorAlertEnabled(config.getErrorAlertEnabled()) + .connectionFailureAlertEnabled(config.getConnectionFailureAlertEnabled()) + .connectionFailureThreshold(config.getConnectionFailureThreshold()) + .connectionFailureWindowMinutes(config.getConnectionFailureWindowMinutes()) + .emailNotificationsEnabled(config.getEmailNotificationsEnabled()) + .pushNotificationsEnabled(config.getPushNotificationsEnabled()) + .smsNotificationsEnabled(config.getSmsNotificationsEnabled()) + .alertEmailRecipients(config.getAlertEmailRecipients()) + .totalAlertsLast24h((int) totalLast24h) + .activeAlerts((int) activeAlerts) + .acknowledgedAlerts((int) acknowledgedLast24h) .build(); } /** * Mettre à jour la configuration des alertes */ + @Transactional public AlertConfigResponse updateAlertConfig(UpdateAlertConfigRequest request) { log.info("Mise à jour de la configuration des alertes"); - // Dans une vraie implémentation, on persisterait en DB - // Pour l'instant, on retourne juste la config avec les nouvelles valeurs + // Mapper request vers entité + AlertConfiguration config = new AlertConfiguration(); + config.setCpuHighAlertEnabled(request.getCpuHighAlertEnabled()); + config.setCpuThresholdPercent(request.getCpuThresholdPercent()); + config.setCpuDurationMinutes(request.getCpuDurationMinutes()); + config.setMemoryLowAlertEnabled(request.getMemoryLowAlertEnabled()); + config.setMemoryThresholdPercent(request.getMemoryThresholdPercent()); + config.setCriticalErrorAlertEnabled(request.getCriticalErrorAlertEnabled()); + config.setErrorAlertEnabled(request.getErrorAlertEnabled()); + config.setConnectionFailureAlertEnabled(request.getConnectionFailureAlertEnabled()); + config.setConnectionFailureThreshold(request.getConnectionFailureThreshold()); + config.setConnectionFailureWindowMinutes(request.getConnectionFailureWindowMinutes()); + config.setEmailNotificationsEnabled(request.getEmailNotificationsEnabled()); + config.setPushNotificationsEnabled(request.getPushNotificationsEnabled()); + config.setSmsNotificationsEnabled(request.getSmsNotificationsEnabled()); + config.setAlertEmailRecipients(request.getAlertEmailRecipients()); + // Persister + AlertConfiguration updated = alertConfigurationRepository.updateConfiguration(config); + + // Compter les alertes + long totalLast24h = systemAlertRepository.countLast24h(); + long activeAlerts = systemAlertRepository.countActive(); + long acknowledgedLast24h = systemAlertRepository.countAcknowledgedLast24h(); + + // Mapper vers response return AlertConfigResponse.builder() - .cpuHighAlertEnabled(request.getCpuHighAlertEnabled()) - .cpuThresholdPercent(request.getCpuThresholdPercent()) - .cpuDurationMinutes(request.getCpuDurationMinutes()) - .memoryLowAlertEnabled(request.getMemoryLowAlertEnabled()) - .memoryThresholdPercent(request.getMemoryThresholdPercent()) - .criticalErrorAlertEnabled(request.getCriticalErrorAlertEnabled()) - .errorAlertEnabled(request.getErrorAlertEnabled()) - .connectionFailureAlertEnabled(request.getConnectionFailureAlertEnabled()) - .connectionFailureThreshold(request.getConnectionFailureThreshold()) - .connectionFailureWindowMinutes(request.getConnectionFailureWindowMinutes()) - .emailNotificationsEnabled(request.getEmailNotificationsEnabled()) - .pushNotificationsEnabled(request.getPushNotificationsEnabled()) - .smsNotificationsEnabled(request.getSmsNotificationsEnabled()) - .alertEmailRecipients(request.getAlertEmailRecipients()) - .totalAlertsLast24h(15) - .activeAlerts(2) - .acknowledgedAlerts(13) + .cpuHighAlertEnabled(updated.getCpuHighAlertEnabled()) + .cpuThresholdPercent(updated.getCpuThresholdPercent()) + .cpuDurationMinutes(updated.getCpuDurationMinutes()) + .memoryLowAlertEnabled(updated.getMemoryLowAlertEnabled()) + .memoryThresholdPercent(updated.getMemoryThresholdPercent()) + .criticalErrorAlertEnabled(updated.getCriticalErrorAlertEnabled()) + .errorAlertEnabled(updated.getErrorAlertEnabled()) + .connectionFailureAlertEnabled(updated.getConnectionFailureAlertEnabled()) + .connectionFailureThreshold(updated.getConnectionFailureThreshold()) + .connectionFailureWindowMinutes(updated.getConnectionFailureWindowMinutes()) + .emailNotificationsEnabled(updated.getEmailNotificationsEnabled()) + .pushNotificationsEnabled(updated.getPushNotificationsEnabled()) + .smsNotificationsEnabled(updated.getSmsNotificationsEnabled()) + .alertEmailRecipients(updated.getAlertEmailRecipients()) + .totalAlertsLast24h((int) totalLast24h) + .activeAlerts((int) activeAlerts) + .acknowledgedAlerts((int) acknowledgedLast24h) .build(); } + // ==================== MÉTHODES DE MAPPING ==================== + /** - * Générer des logs de test (à remplacer par une vraie source de logs) + * Mapper SystemLog vers SystemLogResponse */ - private List generateMockLogs() { - List logs = new ArrayList<>(); + private SystemLogResponse mapToLogResponse(SystemLog log) { + SystemLogResponse response = SystemLogResponse.builder() + .level(log.getLevel()) + .source(log.getSource()) + .message(log.getMessage()) + .details(log.getDetails()) + .timestamp(log.getTimestamp()) + .userId(log.getUserId()) + .ipAddress(log.getIpAddress()) + .sessionId(log.getSessionId()) + .endpoint(log.getEndpoint()) + .httpStatusCode(log.getHttpStatusCode()) + .build(); + response.setId(log.getId()); + return response; + } - logs.add(SystemLogResponse.builder() - .id(UUID.randomUUID()) - .level("CRITICAL") - .source("Database") - .message("Connexion à la base de données perdue") - .details("Pool de connexions épuisé") - .timestamp(LocalDateTime.now().minusMinutes(15)) - .username("system") - .ipAddress("192.168.1.100") - .requestId(UUID.randomUUID().toString()) - .build()); - - logs.add(SystemLogResponse.builder() - .id(UUID.randomUUID()) - .level("ERROR") - .source("API") - .message("Erreur 500 sur /api/members") - .details("NullPointerException dans MemberService.findAll()") - .timestamp(LocalDateTime.now().minusMinutes(18)) - .username("admin@test.com") - .ipAddress("192.168.1.101") - .requestId(UUID.randomUUID().toString()) - .stackTrace("java.lang.NullPointerException\n\tat dev.lions.unionflow.server.service.MemberService.findAll(MemberService.java:45)") - .build()); - - logs.add(SystemLogResponse.builder() - .id(UUID.randomUUID()) - .level("WARN") - .source("Auth") - .message("Tentative de connexion avec mot de passe incorrect") - .details("IP: 192.168.1.100 - Utilisateur: admin@test.com") - .timestamp(LocalDateTime.now().minusMinutes(20)) - .username("admin@test.com") - .ipAddress("192.168.1.100") - .requestId(UUID.randomUUID().toString()) - .build()); - - logs.add(SystemLogResponse.builder() - .id(UUID.randomUUID()) - .level("INFO") - .source("System") - .message("Sauvegarde automatique terminée") - .details("Taille: 2.3 GB - Durée: 45 secondes") - .timestamp(LocalDateTime.now().minusHours(2)) - .username("system") - .ipAddress("localhost") - .requestId(UUID.randomUUID().toString()) - .build()); - - logs.add(SystemLogResponse.builder() - .id(UUID.randomUUID()) - .level("DEBUG") - .source("Cache") - .message("Cache invalidé pour user_sessions") - .details("Raison: Expiration automatique") - .timestamp(LocalDateTime.now().minusMinutes(25)) - .username("system") - .ipAddress("localhost") - .requestId(UUID.randomUUID().toString()) - .build()); - - logs.add(SystemLogResponse.builder() - .id(UUID.randomUUID()) - .level("TRACE") - .source("Performance") - .message("Requête SQL exécutée") - .details("SELECT * FROM members WHERE active = true - Durée: 23ms") - .timestamp(LocalDateTime.now().minusMinutes(27)) - .username("system") - .ipAddress("localhost") - .requestId(UUID.randomUUID().toString()) - .build()); - - return logs; + /** + * Mapper SystemAlert vers SystemAlertResponse + */ + private SystemAlertResponse mapToAlertResponse(SystemAlert alert) { + SystemAlertResponse response = SystemAlertResponse.builder() + .level(alert.getLevel()) + .title(alert.getTitle()) + .message(alert.getMessage()) + .timestamp(alert.getTimestamp()) + .acknowledged(alert.getAcknowledged()) + .acknowledgedBy(alert.getAcknowledgedBy()) + .acknowledgedAt(alert.getAcknowledgedAt()) + .source(alert.getSource()) + .alertType(alert.getAlertType()) + .currentValue(alert.getCurrentValue()) + .thresholdValue(alert.getThresholdValue()) + .unit(alert.getUnit()) + .recommendedActions(alert.getRecommendedActions()) + .build(); + response.setId(alert.getId()); + return response; } /** diff --git a/src/main/java/dev/lions/unionflow/server/service/SystemLoggingService.java b/src/main/java/dev/lions/unionflow/server/service/SystemLoggingService.java new file mode 100644 index 0000000..7a3b2d2 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/SystemLoggingService.java @@ -0,0 +1,209 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.entity.SystemLog; +import dev.lions.unionflow.server.repository.SystemLogRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; + +/** + * Service centralisé pour la création de logs système. + * Gère la persistence des logs dans la base de données. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2026-03-15 + */ +@Slf4j +@ApplicationScoped +public class SystemLoggingService { + + @Inject + SystemLogRepository systemLogRepository; + + /** + * Logger une requête HTTP + */ + @Transactional + public void logRequest( + String method, + String endpoint, + Integer httpStatusCode, + String userId, + String ipAddress, + String sessionId, + Long durationMs + ) { + try { + SystemLog systemLog = new SystemLog(); + systemLog.setLevel(getLogLevelFromStatusCode(httpStatusCode)); + systemLog.setSource("API"); + systemLog.setMessage(String.format("%s %s - %d (%dms)", method, endpoint, httpStatusCode, durationMs)); + systemLog.setDetails(String.format("User: %s, IP: %s, Duration: %dms", userId, ipAddress, durationMs)); + systemLog.setTimestamp(LocalDateTime.now()); + systemLog.setUserId(userId); + systemLog.setIpAddress(ipAddress); + systemLog.setSessionId(sessionId); + systemLog.setEndpoint(endpoint); + systemLog.setHttpStatusCode(httpStatusCode); + + systemLogRepository.persist(systemLog); + } catch (Exception e) { + // Ne pas propager les erreurs de logging pour ne pas casser l'application + log.error("Failed to persist request log", e); + } + } + + /** + * Logger une erreur (exception) + */ + @Transactional + public void logError( + String source, + String message, + String details, + String userId, + String ipAddress, + String endpoint, + Integer httpStatusCode + ) { + try { + SystemLog systemLog = new SystemLog(); + systemLog.setLevel("ERROR"); + systemLog.setSource(source); + systemLog.setMessage(message); + systemLog.setDetails(details); + systemLog.setTimestamp(LocalDateTime.now()); + systemLog.setUserId(userId); + systemLog.setIpAddress(ipAddress); + systemLog.setEndpoint(endpoint); + systemLog.setHttpStatusCode(httpStatusCode); + + systemLogRepository.persist(systemLog); + } catch (Exception e) { + log.error("Failed to persist error log", e); + } + } + + /** + * Logger une erreur critique + */ + @Transactional + public void logCritical( + String source, + String message, + String details, + String userId, + String ipAddress + ) { + try { + SystemLog systemLog = new SystemLog(); + systemLog.setLevel("CRITICAL"); + systemLog.setSource(source); + systemLog.setMessage(message); + systemLog.setDetails(details); + systemLog.setTimestamp(LocalDateTime.now()); + systemLog.setUserId(userId); + systemLog.setIpAddress(ipAddress); + + systemLogRepository.persist(systemLog); + } catch (Exception e) { + log.error("Failed to persist critical log", e); + } + } + + /** + * Logger un warning + */ + @Transactional + public void logWarning( + String source, + String message, + String details, + String userId, + String ipAddress + ) { + try { + SystemLog systemLog = new SystemLog(); + systemLog.setLevel("WARNING"); + systemLog.setSource(source); + systemLog.setMessage(message); + systemLog.setDetails(details); + systemLog.setTimestamp(LocalDateTime.now()); + systemLog.setUserId(userId); + systemLog.setIpAddress(ipAddress); + + systemLogRepository.persist(systemLog); + } catch (Exception e) { + log.error("Failed to persist warning log", e); + } + } + + /** + * Logger une info + */ + @Transactional + public void logInfo( + String source, + String message, + String details + ) { + try { + SystemLog systemLog = new SystemLog(); + systemLog.setLevel("INFO"); + systemLog.setSource(source); + systemLog.setMessage(message); + systemLog.setDetails(details); + systemLog.setTimestamp(LocalDateTime.now()); + + systemLogRepository.persist(systemLog); + } catch (Exception e) { + log.error("Failed to persist info log", e); + } + } + + /** + * Logger un événement de debug + */ + @Transactional + public void logDebug( + String source, + String message, + String details + ) { + try { + SystemLog systemLog = new SystemLog(); + systemLog.setLevel("DEBUG"); + systemLog.setSource(source); + systemLog.setMessage(message); + systemLog.setDetails(details); + systemLog.setTimestamp(LocalDateTime.now()); + + systemLogRepository.persist(systemLog); + } catch (Exception e) { + log.error("Failed to persist debug log", e); + } + } + + /** + * Déterminer le niveau de log selon le code HTTP + */ + private String getLogLevelFromStatusCode(Integer statusCode) { + if (statusCode == null) { + return "INFO"; + } + + if (statusCode >= 500) { + return "ERROR"; + } else if (statusCode >= 400) { + return "WARNING"; + } else if (statusCode >= 300) { + return "INFO"; + } else { + return "DEBUG"; + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/SystemMetricsService.java b/src/main/java/dev/lions/unionflow/server/service/SystemMetricsService.java index 0825348..b7ad320 100644 --- a/src/main/java/dev/lions/unionflow/server/service/SystemMetricsService.java +++ b/src/main/java/dev/lions/unionflow/server/service/SystemMetricsService.java @@ -1,6 +1,6 @@ package dev.lions.unionflow.server.service; -import dev.lions.unionflow.server.api.dto.system.response.SystemMetricsResponse; +import dev.lions.unionflow.server.api.dto.logs.response.SystemMetricsResponse; import dev.lions.unionflow.server.entity.Membre; import dev.lions.unionflow.server.repository.MembreRepository; import io.agroal.api.AgroalDataSource; diff --git a/src/main/resources/db/migration/V1__UnionFlow_Complete_Schema.sql b/src/main/resources/db/migration/V1__UnionFlow_Complete_Schema.sql index 07925a2..66cd92c 100644 --- a/src/main/resources/db/migration/V1__UnionFlow_Complete_Schema.sql +++ b/src/main/resources/db/migration/V1__UnionFlow_Complete_Schema.sql @@ -1,843 +1,16 @@ --- UnionFlow : schema complet (consolidation des migrations V1.2 a V3.7) --- Nouvelle base : ce script suffit. Bases existantes : voir README_CONSOLIDATION.md - --- ========== V1.2__Create_Organisation_Table.sql ========== --- Migration V1.2: Création de la table organisations --- Auteur: UnionFlow Team --- Date: 2025-01-15 --- Description: Création de la table organisations avec toutes les colonnes nécessaires - --- Création de la table organisations -CREATE TABLE organisations ( - id BIGSERIAL PRIMARY KEY, - - -- Informations de base - nom VARCHAR(200) NOT NULL, - nom_court VARCHAR(50), - type_organisation VARCHAR(50) NOT NULL DEFAULT 'ASSOCIATION', - statut VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', - description TEXT, - date_fondation DATE, - numero_enregistrement VARCHAR(100) UNIQUE, - - -- Informations de contact - email VARCHAR(255) NOT NULL UNIQUE, - telephone VARCHAR(20), - telephone_secondaire VARCHAR(20), - email_secondaire VARCHAR(255), - - -- Adresse - adresse VARCHAR(500), - ville VARCHAR(100), - code_postal VARCHAR(20), - region VARCHAR(100), - pays VARCHAR(100), - - -- Coordonnées géographiques - latitude DECIMAL(9,6) CHECK (latitude >= -90 AND latitude <= 90), - longitude DECIMAL(9,6) CHECK (longitude >= -180 AND longitude <= 180), - - -- Web et réseaux sociaux - site_web VARCHAR(500), - logo VARCHAR(500), - reseaux_sociaux VARCHAR(1000), - - -- Hiérarchie - organisation_parente_id UUID, - niveau_hierarchique INTEGER NOT NULL DEFAULT 0, - - -- Statistiques - nombre_membres INTEGER NOT NULL DEFAULT 0, - nombre_administrateurs INTEGER NOT NULL DEFAULT 0, - - -- Finances - budget_annuel DECIMAL(14,2) CHECK (budget_annuel >= 0), - devise VARCHAR(3) DEFAULT 'XOF', - cotisation_obligatoire BOOLEAN NOT NULL DEFAULT FALSE, - montant_cotisation_annuelle DECIMAL(12,2) CHECK (montant_cotisation_annuelle >= 0), - - -- Informations complémentaires - objectifs TEXT, - activites_principales TEXT, - certifications VARCHAR(500), - partenaires VARCHAR(1000), - notes VARCHAR(1000), - - -- Paramètres - organisation_publique BOOLEAN NOT NULL DEFAULT TRUE, - accepte_nouveaux_membres BOOLEAN NOT NULL DEFAULT TRUE, - - -- Métadonnées - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(100), - modifie_par VARCHAR(100), - version BIGINT NOT NULL DEFAULT 0, - - -- Contraintes - CONSTRAINT chk_organisation_statut CHECK (statut IN ('ACTIVE', 'SUSPENDUE', 'DISSOUTE', 'EN_ATTENTE')), - CONSTRAINT chk_organisation_type CHECK (type_organisation IN ( - 'ASSOCIATION', 'LIONS_CLUB', 'ROTARY_CLUB', 'COOPERATIVE', - 'FONDATION', 'ONG', 'SYNDICAT', 'AUTRE' - )), - CONSTRAINT chk_organisation_devise CHECK (devise IN ('XOF', 'EUR', 'USD', 'GBP', 'CHF')), - CONSTRAINT chk_organisation_niveau CHECK (niveau_hierarchique >= 0 AND niveau_hierarchique <= 10), - CONSTRAINT chk_organisation_membres CHECK (nombre_membres >= 0), - CONSTRAINT chk_organisation_admins CHECK (nombre_administrateurs >= 0) -); - --- Création des index pour optimiser les performances -CREATE INDEX idx_organisation_nom ON organisations(nom); -CREATE INDEX idx_organisation_email ON organisations(email); -CREATE INDEX idx_organisation_statut ON organisations(statut); -CREATE INDEX idx_organisation_type ON organisations(type_organisation); -CREATE INDEX idx_organisation_ville ON organisations(ville); -CREATE INDEX idx_organisation_pays ON organisations(pays); -CREATE INDEX idx_organisation_parente ON organisations(organisation_parente_id); -CREATE INDEX idx_organisation_numero_enregistrement ON organisations(numero_enregistrement); -CREATE INDEX idx_organisation_actif ON organisations(actif); -CREATE INDEX idx_organisation_date_creation ON organisations(date_creation); -CREATE INDEX idx_organisation_publique ON organisations(organisation_publique); -CREATE INDEX idx_organisation_accepte_membres ON organisations(accepte_nouveaux_membres); - --- Index composites pour les recherches fréquentes -CREATE INDEX idx_organisation_statut_actif ON organisations(statut, actif); -CREATE INDEX idx_organisation_type_ville ON organisations(type_organisation, ville); -CREATE INDEX idx_organisation_pays_region ON organisations(pays, region); -CREATE INDEX idx_organisation_publique_actif ON organisations(organisation_publique, actif); - --- Index pour les recherches textuelles -CREATE INDEX idx_organisation_nom_lower ON organisations(LOWER(nom)); -CREATE INDEX idx_organisation_nom_court_lower ON organisations(LOWER(nom_court)); -CREATE INDEX idx_organisation_ville_lower ON organisations(LOWER(ville)); - --- Ajout de la colonne organisation_id à la table membres (si la table et la colonne existent) -DO $$ -BEGIN - -- Vérifier d'abord si la table membres existe - IF EXISTS ( - SELECT 1 FROM information_schema.tables - WHERE table_name = 'membres' - ) THEN - -- Puis vérifier si la colonne organisation_id n'existe pas déjà - IF NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'membres' AND column_name = 'organisation_id' - ) THEN - ALTER TABLE membres ADD COLUMN organisation_id BIGINT; - ALTER TABLE membres ADD CONSTRAINT fk_membre_organisation - FOREIGN KEY (organisation_id) REFERENCES organisations(id); - CREATE INDEX idx_membre_organisation ON membres(organisation_id); - END IF; - END IF; -END $$; - --- IMPORTANT: Aucune donnée fictive n'est insérée dans ce script de migration. --- Les données doivent être insérées manuellement via l'interface d'administration --- ou via des scripts de migration séparés si nécessaire pour la production. - --- Mise à jour des statistiques de la base de données -ANALYZE organisations; - --- Commentaires sur la table et les colonnes principales -COMMENT ON TABLE organisations IS 'Table des organisations (Lions Clubs, Associations, Coopératives, etc.)'; -COMMENT ON COLUMN organisations.nom IS 'Nom officiel de l''organisation'; -COMMENT ON COLUMN organisations.nom_court IS 'Nom court ou sigle de l''organisation'; -COMMENT ON COLUMN organisations.type_organisation IS 'Type d''organisation (LIONS_CLUB, ASSOCIATION, etc.)'; -COMMENT ON COLUMN organisations.statut IS 'Statut actuel de l''organisation (ACTIVE, SUSPENDUE, etc.)'; -COMMENT ON COLUMN organisations.organisation_parente_id IS 'ID de l''organisation parente pour la hiérarchie'; -COMMENT ON COLUMN organisations.niveau_hierarchique IS 'Niveau dans la hiérarchie (0 = racine)'; -COMMENT ON COLUMN organisations.nombre_membres IS 'Nombre total de membres actifs'; -COMMENT ON COLUMN organisations.organisation_publique IS 'Si l''organisation est visible publiquement'; -COMMENT ON COLUMN organisations.accepte_nouveaux_membres IS 'Si l''organisation accepte de nouveaux membres'; -COMMENT ON COLUMN organisations.version IS 'Version pour le contrôle de concurrence optimiste'; - - --- ========== V1.3__Convert_Ids_To_UUID.sql ========== --- Migration V1.3: Conversion des colonnes ID de BIGINT vers UUID --- Auteur: UnionFlow Team --- Date: 2025-01-16 --- Description: Convertit toutes les colonnes ID et clés étrangères de BIGINT vers UUID --- ATTENTION: Cette migration supprime toutes les données existantes pour simplifier la conversion --- Pour une migration avec préservation des données, voir V1.3.1__Convert_Ids_To_UUID_With_Data.sql - --- ============================================ --- ÉTAPE 1: Suppression des contraintes de clés étrangères --- ============================================ - --- Supprimer les contraintes de clés étrangères existantes -DO $$ -BEGIN - -- Supprimer FK membres -> organisations - IF EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE constraint_name = 'fk_membre_organisation' - AND table_name = 'membres' - ) THEN - ALTER TABLE membres DROP CONSTRAINT fk_membre_organisation; - END IF; - - -- Supprimer FK cotisations -> membres - IF EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE constraint_name LIKE 'fk_cotisation%' - AND table_name = 'cotisations' - ) THEN - ALTER TABLE cotisations DROP CONSTRAINT IF EXISTS fk_cotisation_membre CASCADE; - END IF; - - -- Supprimer FK evenements -> organisations - IF EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE constraint_name LIKE 'fk_evenement%' - AND table_name = 'evenements' - ) THEN - ALTER TABLE evenements DROP CONSTRAINT IF EXISTS fk_evenement_organisation CASCADE; - END IF; - - -- Supprimer FK inscriptions_evenement -> membres et evenements - IF EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE constraint_name LIKE 'fk_inscription%' - AND table_name = 'inscriptions_evenement' - ) THEN - ALTER TABLE inscriptions_evenement DROP CONSTRAINT IF EXISTS fk_inscription_membre CASCADE; - ALTER TABLE inscriptions_evenement DROP CONSTRAINT IF EXISTS fk_inscription_evenement CASCADE; - END IF; - - -- Supprimer FK demandes_aide -> membres et organisations - IF EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE constraint_name LIKE 'fk_demande%' - AND table_name = 'demandes_aide' - ) THEN - ALTER TABLE demandes_aide DROP CONSTRAINT IF EXISTS fk_demande_demandeur CASCADE; - ALTER TABLE demandes_aide DROP CONSTRAINT IF EXISTS fk_demande_evaluateur CASCADE; - ALTER TABLE demandes_aide DROP CONSTRAINT IF EXISTS fk_demande_organisation CASCADE; - END IF; -END $$; - --- ============================================ --- ÉTAPE 2: Supprimer les séquences (BIGSERIAL) --- ============================================ - -DROP SEQUENCE IF EXISTS membres_SEQ CASCADE; -DROP SEQUENCE IF EXISTS cotisations_SEQ CASCADE; -DROP SEQUENCE IF EXISTS evenements_SEQ CASCADE; -DROP SEQUENCE IF EXISTS organisations_id_seq CASCADE; - --- ============================================ --- ÉTAPE 3: Supprimer les tables existantes (pour recréation avec UUID) --- ============================================ - --- Supprimer les tables dans l'ordre inverse des dépendances -DROP TABLE IF EXISTS inscriptions_evenement CASCADE; -DROP TABLE IF EXISTS demandes_aide CASCADE; -DROP TABLE IF EXISTS cotisations CASCADE; -DROP TABLE IF EXISTS evenements CASCADE; -DROP TABLE IF EXISTS membres CASCADE; -DROP TABLE IF EXISTS organisations CASCADE; - --- ============================================ --- ÉTAPE 4: Recréer les tables avec UUID --- ============================================ - --- Table organisations avec UUID -CREATE TABLE organisations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Informations de base - nom VARCHAR(200) NOT NULL, - nom_court VARCHAR(50), - type_organisation VARCHAR(50) NOT NULL DEFAULT 'ASSOCIATION', - statut VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', - description TEXT, - date_fondation DATE, - numero_enregistrement VARCHAR(100) UNIQUE, - - -- Informations de contact - email VARCHAR(255) NOT NULL UNIQUE, - telephone VARCHAR(20), - telephone_secondaire VARCHAR(20), - email_secondaire VARCHAR(255), - - -- Adresse - adresse VARCHAR(500), - ville VARCHAR(100), - code_postal VARCHAR(20), - region VARCHAR(100), - pays VARCHAR(100), - - -- Coordonnées géographiques - latitude DECIMAL(9,6) CHECK (latitude >= -90 AND latitude <= 90), - longitude DECIMAL(9,6) CHECK (longitude >= -180 AND longitude <= 180), - - -- Web et réseaux sociaux - site_web VARCHAR(500), - logo VARCHAR(500), - reseaux_sociaux VARCHAR(1000), - - -- Hiérarchie - organisation_parente_id UUID, - niveau_hierarchique INTEGER NOT NULL DEFAULT 0, - - -- Statistiques - nombre_membres INTEGER NOT NULL DEFAULT 0, - nombre_administrateurs INTEGER NOT NULL DEFAULT 0, - - -- Finances - budget_annuel DECIMAL(14,2) CHECK (budget_annuel >= 0), - devise VARCHAR(3) DEFAULT 'XOF', - cotisation_obligatoire BOOLEAN NOT NULL DEFAULT FALSE, - montant_cotisation_annuelle DECIMAL(12,2) CHECK (montant_cotisation_annuelle >= 0), - - -- Informations complémentaires - objectifs TEXT, - activites_principales TEXT, - certifications VARCHAR(500), - partenaires VARCHAR(1000), - notes VARCHAR(1000), - - -- Paramètres - organisation_publique BOOLEAN NOT NULL DEFAULT TRUE, - accepte_nouveaux_membres BOOLEAN NOT NULL DEFAULT TRUE, - - -- Métadonnées (héritées de BaseEntity) - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - -- Contraintes - CONSTRAINT chk_organisation_statut CHECK (statut IN ('ACTIVE', 'SUSPENDUE', 'DISSOUTE', 'EN_ATTENTE')), - CONSTRAINT chk_organisation_type CHECK (type_organisation IN ( - 'ASSOCIATION', 'LIONS_CLUB', 'ROTARY_CLUB', 'COOPERATIVE', - 'FONDATION', 'ONG', 'SYNDICAT', 'AUTRE' - )), - CONSTRAINT chk_organisation_devise CHECK (devise IN ('XOF', 'EUR', 'USD', 'GBP', 'CHF')), - CONSTRAINT chk_organisation_niveau CHECK (niveau_hierarchique >= 0 AND niveau_hierarchique <= 10), - CONSTRAINT chk_organisation_membres CHECK (nombre_membres >= 0), - CONSTRAINT chk_organisation_admins CHECK (nombre_administrateurs >= 0), - - -- Clé étrangère pour hiérarchie - CONSTRAINT fk_organisation_parente FOREIGN KEY (organisation_parente_id) - REFERENCES organisations(id) ON DELETE SET NULL -); - --- Table membres avec UUID -CREATE TABLE membres ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - numero_membre VARCHAR(20) UNIQUE NOT NULL, - prenom VARCHAR(100) NOT NULL, - nom VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - mot_de_passe VARCHAR(255), - telephone VARCHAR(20), - date_naissance DATE NOT NULL, - date_adhesion DATE NOT NULL, - roles VARCHAR(500), - - -- Clé étrangère vers organisations - organisation_id UUID, - - -- Métadonnées (héritées de BaseEntity) - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_membre_organisation FOREIGN KEY (organisation_id) - REFERENCES organisations(id) ON DELETE SET NULL -); - --- Table cotisations avec UUID -CREATE TABLE cotisations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - numero_reference VARCHAR(50) UNIQUE NOT NULL, - membre_id UUID NOT NULL, - type_cotisation VARCHAR(50) NOT NULL, - montant_du DECIMAL(12,2) NOT NULL CHECK (montant_du >= 0), - montant_paye DECIMAL(12,2) NOT NULL DEFAULT 0 CHECK (montant_paye >= 0), - code_devise VARCHAR(3) NOT NULL DEFAULT 'XOF', - statut VARCHAR(30) NOT NULL, - date_echeance DATE NOT NULL, - date_paiement TIMESTAMP, - description VARCHAR(500), - periode VARCHAR(20), - annee INTEGER NOT NULL CHECK (annee >= 2020 AND annee <= 2100), - mois INTEGER CHECK (mois >= 1 AND mois <= 12), - observations VARCHAR(1000), - recurrente BOOLEAN NOT NULL DEFAULT FALSE, - nombre_rappels INTEGER NOT NULL DEFAULT 0 CHECK (nombre_rappels >= 0), - date_dernier_rappel TIMESTAMP, - valide_par_id UUID, - nom_validateur VARCHAR(100), - date_validation TIMESTAMP, - methode_paiement VARCHAR(50), - reference_paiement VARCHAR(100), - - -- Métadonnées (héritées de BaseEntity) - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_cotisation_membre FOREIGN KEY (membre_id) - REFERENCES membres(id) ON DELETE CASCADE, - CONSTRAINT chk_cotisation_statut CHECK (statut IN ('EN_ATTENTE', 'PAYEE', 'EN_RETARD', 'PARTIELLEMENT_PAYEE', 'ANNULEE')), - CONSTRAINT chk_cotisation_devise CHECK (code_devise ~ '^[A-Z]{3}$') -); - --- Table evenements avec UUID -CREATE TABLE evenements ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - titre VARCHAR(200) NOT NULL, - description VARCHAR(2000), - date_debut TIMESTAMP NOT NULL, - date_fin TIMESTAMP, - lieu VARCHAR(255) NOT NULL, - adresse VARCHAR(500), - ville VARCHAR(100), - pays VARCHAR(100), - code_postal VARCHAR(20), - latitude DECIMAL(9,6), - longitude DECIMAL(9,6), - type_evenement VARCHAR(50) NOT NULL, - statut VARCHAR(50) NOT NULL, - url_inscription VARCHAR(500), - url_informations VARCHAR(500), - image_url VARCHAR(500), - capacite_max INTEGER, - cout_participation DECIMAL(12,2), - devise VARCHAR(3), - est_public BOOLEAN NOT NULL DEFAULT TRUE, - tags VARCHAR(500), - notes VARCHAR(1000), - - -- Clé étrangère vers organisations - organisation_id UUID, - - -- Métadonnées (héritées de BaseEntity) - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_evenement_organisation FOREIGN KEY (organisation_id) - REFERENCES organisations(id) ON DELETE SET NULL -); - --- Table inscriptions_evenement avec UUID -CREATE TABLE inscriptions_evenement ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - membre_id UUID NOT NULL, - evenement_id UUID NOT NULL, - date_inscription TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - statut VARCHAR(20) DEFAULT 'CONFIRMEE', - commentaire VARCHAR(500), - - -- Métadonnées (héritées de BaseEntity) - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_inscription_membre FOREIGN KEY (membre_id) - REFERENCES membres(id) ON DELETE CASCADE, - CONSTRAINT fk_inscription_evenement FOREIGN KEY (evenement_id) - REFERENCES evenements(id) ON DELETE CASCADE, - CONSTRAINT chk_inscription_statut CHECK (statut IN ('CONFIRMEE', 'EN_ATTENTE', 'ANNULEE', 'REFUSEE')), - CONSTRAINT uk_inscription_membre_evenement UNIQUE (membre_id, evenement_id) -); - --- Table demandes_aide avec UUID -CREATE TABLE demandes_aide ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - titre VARCHAR(200) NOT NULL, - description TEXT NOT NULL, - type_aide VARCHAR(50) NOT NULL, - statut VARCHAR(50) NOT NULL, - montant_demande DECIMAL(10,2), - montant_approuve DECIMAL(10,2), - date_demande TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_evaluation TIMESTAMP, - date_versement TIMESTAMP, - justification TEXT, - commentaire_evaluation TEXT, - urgence BOOLEAN NOT NULL DEFAULT FALSE, - documents_fournis VARCHAR(500), - - -- Clés étrangères - demandeur_id UUID NOT NULL, - evaluateur_id UUID, - organisation_id UUID NOT NULL, - - -- Métadonnées (héritées de BaseEntity) - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_demande_demandeur FOREIGN KEY (demandeur_id) - REFERENCES membres(id) ON DELETE CASCADE, - CONSTRAINT fk_demande_evaluateur FOREIGN KEY (evaluateur_id) - REFERENCES membres(id) ON DELETE SET NULL, - CONSTRAINT fk_demande_organisation FOREIGN KEY (organisation_id) - REFERENCES organisations(id) ON DELETE CASCADE -); - --- ============================================ --- ÉTAPE 5: Recréer les index --- ============================================ - --- Index pour organisations -CREATE INDEX idx_organisation_nom ON organisations(nom); -CREATE INDEX idx_organisation_email ON organisations(email); -CREATE INDEX idx_organisation_statut ON organisations(statut); -CREATE INDEX idx_organisation_type ON organisations(type_organisation); -CREATE INDEX idx_organisation_ville ON organisations(ville); -CREATE INDEX idx_organisation_pays ON organisations(pays); -CREATE INDEX idx_organisation_parente ON organisations(organisation_parente_id); -CREATE INDEX idx_organisation_numero_enregistrement ON organisations(numero_enregistrement); -CREATE INDEX idx_organisation_actif ON organisations(actif); -CREATE INDEX idx_organisation_date_creation ON organisations(date_creation); -CREATE INDEX idx_organisation_publique ON organisations(organisation_publique); -CREATE INDEX idx_organisation_accepte_membres ON organisations(accepte_nouveaux_membres); -CREATE INDEX idx_organisation_statut_actif ON organisations(statut, actif); - --- Index pour membres -CREATE INDEX idx_membre_email ON membres(email); -CREATE INDEX idx_membre_numero ON membres(numero_membre); -CREATE INDEX idx_membre_actif ON membres(actif); -CREATE INDEX idx_membre_organisation ON membres(organisation_id); - --- Index pour cotisations -CREATE INDEX idx_cotisation_membre ON cotisations(membre_id); -CREATE INDEX idx_cotisation_reference ON cotisations(numero_reference); -CREATE INDEX idx_cotisation_statut ON cotisations(statut); -CREATE INDEX idx_cotisation_echeance ON cotisations(date_echeance); -CREATE INDEX idx_cotisation_type ON cotisations(type_cotisation); -CREATE INDEX idx_cotisation_annee_mois ON cotisations(annee, mois); - --- Index pour evenements -CREATE INDEX idx_evenement_date_debut ON evenements(date_debut); -CREATE INDEX idx_evenement_statut ON evenements(statut); -CREATE INDEX idx_evenement_type ON evenements(type_evenement); -CREATE INDEX idx_evenement_organisation ON evenements(organisation_id); - --- Index pour inscriptions_evenement -CREATE INDEX idx_inscription_membre ON inscriptions_evenement(membre_id); -CREATE INDEX idx_inscription_evenement ON inscriptions_evenement(evenement_id); -CREATE INDEX idx_inscription_date ON inscriptions_evenement(date_inscription); - --- Index pour demandes_aide -CREATE INDEX idx_demande_demandeur ON demandes_aide(demandeur_id); -CREATE INDEX idx_demande_evaluateur ON demandes_aide(evaluateur_id); -CREATE INDEX idx_demande_organisation ON demandes_aide(organisation_id); -CREATE INDEX idx_demande_statut ON demandes_aide(statut); -CREATE INDEX idx_demande_type ON demandes_aide(type_aide); -CREATE INDEX idx_demande_date_demande ON demandes_aide(date_demande); - --- ============================================ --- ÉTAPE 6: Commentaires sur les tables --- ============================================ - -COMMENT ON TABLE organisations IS 'Table des organisations (Lions Clubs, Associations, Coopératives, etc.) avec UUID'; -COMMENT ON TABLE membres IS 'Table des membres avec UUID'; -COMMENT ON TABLE cotisations IS 'Table des cotisations avec UUID'; -COMMENT ON TABLE evenements IS 'Table des événements avec UUID'; -COMMENT ON TABLE inscriptions_evenement IS 'Table des inscriptions aux événements avec UUID'; -COMMENT ON TABLE demandes_aide IS 'Table des demandes d''aide avec UUID'; - -COMMENT ON COLUMN organisations.id IS 'UUID unique de l''organisation'; -COMMENT ON COLUMN membres.id IS 'UUID unique du membre'; -COMMENT ON COLUMN cotisations.id IS 'UUID unique de la cotisation'; -COMMENT ON COLUMN evenements.id IS 'UUID unique de l''événement'; -COMMENT ON COLUMN inscriptions_evenement.id IS 'UUID unique de l''inscription'; -COMMENT ON COLUMN demandes_aide.id IS 'UUID unique de la demande d''aide'; - - - --- ========== V1.4__Add_Profession_To_Membres.sql ========== --- Migration V1.4: Ajout de la colonne profession à la table membres --- Auteur: UnionFlow Team --- Date: 2026-02-19 --- Description: Permet l'autocomplétion et le filtrage par profession (MembreDTO, MembreSearchCriteria) - -ALTER TABLE membres ADD COLUMN IF NOT EXISTS profession VARCHAR(100); -COMMENT ON COLUMN membres.profession IS 'Profession du membre (ex. Ingénieur, Médecin)'; - - --- ========== V1.5__Create_Tickets_Suggestions_Favoris_Configuration_Tables.sql ========== --- Migration V1.4: Création des tables Tickets, Suggestions, Favoris et Configuration --- Auteur: UnionFlow Team --- Date: 2025-12-18 --- Description: Création des tables pour la gestion des tickets support, suggestions utilisateur, favoris et configuration système - --- ============================================ --- TABLE: tickets --- ============================================ -CREATE TABLE IF NOT EXISTS tickets ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Champs de base - numero_ticket VARCHAR(50) NOT NULL UNIQUE, - utilisateur_id UUID NOT NULL, - sujet VARCHAR(255) NOT NULL, - description TEXT, - - -- Classification - categorie VARCHAR(50), -- TECHNIQUE, FONCTIONNALITE, UTILISATION, COMPTE, AUTRE - priorite VARCHAR(50), -- BASSE, NORMALE, HAUTE, URGENTE - statut VARCHAR(50) DEFAULT 'OUVERT', -- OUVERT, EN_COURS, EN_ATTENTE, RESOLU, FERME - - -- Gestion - agent_id UUID, - agent_nom VARCHAR(255), - - -- Dates - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_derniere_reponse TIMESTAMP, - date_resolution TIMESTAMP, - date_fermeture TIMESTAMP, - - -- Statistiques - nb_messages INTEGER DEFAULT 0, - nb_fichiers INTEGER DEFAULT 0, - note_satisfaction INTEGER CHECK (note_satisfaction >= 1 AND note_satisfaction <= 5), - - -- Résolution - resolution TEXT, - - -- Audit (BaseEntity) - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN DEFAULT true NOT NULL, - - -- Indexes - CONSTRAINT fk_ticket_utilisateur FOREIGN KEY (utilisateur_id) REFERENCES membres(id) ON DELETE CASCADE -); - -CREATE INDEX idx_ticket_utilisateur ON tickets(utilisateur_id); -CREATE INDEX idx_ticket_statut ON tickets(statut); -CREATE INDEX idx_ticket_categorie ON tickets(categorie); -CREATE INDEX idx_ticket_numero ON tickets(numero_ticket); -CREATE INDEX idx_ticket_date_creation ON tickets(date_creation DESC); - --- ============================================ --- TABLE: suggestions --- ============================================ -CREATE TABLE IF NOT EXISTS suggestions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Utilisateur - utilisateur_id UUID NOT NULL, - utilisateur_nom VARCHAR(255), - - -- Contenu - titre VARCHAR(255) NOT NULL, - description TEXT, - justification TEXT, - - -- Classification - categorie VARCHAR(50), -- UI, FEATURE, PERFORMANCE, SECURITE, INTEGRATION, MOBILE, REPORTING - priorite_estimee VARCHAR(50), -- BASSE, MOYENNE, HAUTE, CRITIQUE - statut VARCHAR(50) DEFAULT 'NOUVELLE', -- NOUVELLE, EVALUATION, APPROUVEE, DEVELOPPEMENT, IMPLEMENTEE, REJETEE - - -- Statistiques - nb_votes INTEGER DEFAULT 0, - nb_commentaires INTEGER DEFAULT 0, - nb_vues INTEGER DEFAULT 0, - - -- Dates - date_soumission TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_evaluation TIMESTAMP, - date_implementation TIMESTAMP, - - -- Version - version_ciblee VARCHAR(50), - mise_a_jour TEXT, - - -- Audit (BaseEntity) - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN DEFAULT true NOT NULL -); - -CREATE INDEX idx_suggestion_utilisateur ON suggestions(utilisateur_id); -CREATE INDEX idx_suggestion_statut ON suggestions(statut); -CREATE INDEX idx_suggestion_categorie ON suggestions(categorie); -CREATE INDEX idx_suggestion_date_soumission ON suggestions(date_soumission DESC); -CREATE INDEX idx_suggestion_nb_votes ON suggestions(nb_votes DESC); - --- ============================================ --- TABLE: suggestion_votes --- ============================================ -CREATE TABLE IF NOT EXISTS suggestion_votes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - suggestion_id UUID NOT NULL, - utilisateur_id UUID NOT NULL, - date_vote TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - -- Audit - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - actif BOOLEAN DEFAULT true NOT NULL, - - -- Contrainte d'unicité : un utilisateur ne peut voter qu'une fois par suggestion - CONSTRAINT uk_suggestion_vote UNIQUE (suggestion_id, utilisateur_id), - CONSTRAINT fk_vote_suggestion FOREIGN KEY (suggestion_id) REFERENCES suggestions(id) ON DELETE CASCADE -); - -CREATE INDEX idx_vote_suggestion ON suggestion_votes(suggestion_id); -CREATE INDEX idx_vote_utilisateur ON suggestion_votes(utilisateur_id); - --- ============================================ --- TABLE: favoris --- ============================================ -CREATE TABLE IF NOT EXISTS favoris ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Utilisateur - utilisateur_id UUID NOT NULL, - - -- Type et contenu - type_favori VARCHAR(50) NOT NULL, -- PAGE, DOCUMENT, CONTACT, RACCOURCI - titre VARCHAR(255) NOT NULL, - description VARCHAR(1000), - url VARCHAR(1000), - - -- Présentation - icon VARCHAR(100), - couleur VARCHAR(50), - categorie VARCHAR(100), - - -- Organisation - ordre INTEGER DEFAULT 0, - - -- Statistiques - nb_visites INTEGER DEFAULT 0, - derniere_visite TIMESTAMP, - est_plus_utilise BOOLEAN DEFAULT false, - - -- Audit (BaseEntity) - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN DEFAULT true NOT NULL, - - CONSTRAINT fk_favori_utilisateur FOREIGN KEY (utilisateur_id) REFERENCES membres(id) ON DELETE CASCADE -); - -CREATE INDEX idx_favori_utilisateur ON favoris(utilisateur_id); -CREATE INDEX idx_favori_type ON favoris(type_favori); -CREATE INDEX idx_favori_categorie ON favoris(categorie); -CREATE INDEX idx_favori_ordre ON favoris(utilisateur_id, ordre); - --- ============================================ --- TABLE: configurations --- ============================================ -CREATE TABLE IF NOT EXISTS configurations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - -- Clé unique - cle VARCHAR(255) NOT NULL UNIQUE, - - -- Valeur - valeur TEXT, - type VARCHAR(50), -- STRING, NUMBER, BOOLEAN, JSON, DATE - - -- Classification - categorie VARCHAR(50), -- SYSTEME, SECURITE, NOTIFICATION, INTEGRATION, APPEARANCE - description VARCHAR(1000), - - -- Contrôles - modifiable BOOLEAN DEFAULT true, - visible BOOLEAN DEFAULT true, - - -- Métadonnées (JSON stocké en TEXT) - metadonnees TEXT, - - -- Audit (BaseEntity) - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN DEFAULT true NOT NULL -); - -CREATE INDEX idx_config_cle ON configurations(cle); -CREATE INDEX idx_config_categorie ON configurations(categorie); -CREATE INDEX idx_config_visible ON configurations(visible) WHERE visible = true; - --- ============================================ --- COMMENTAIRES --- ============================================ -COMMENT ON TABLE tickets IS 'Table pour la gestion des tickets support'; -COMMENT ON TABLE suggestions IS 'Table pour la gestion des suggestions utilisateur'; -COMMENT ON TABLE suggestion_votes IS 'Table pour gérer les votes sur les suggestions (évite les votes multiples)'; -COMMENT ON TABLE favoris IS 'Table pour la gestion des favoris utilisateur'; -COMMENT ON TABLE configurations IS 'Table pour la gestion de la configuration système'; - - - --- ========== V1.6__Add_Keycloak_Link_To_Membres.sql ========== --- Migration V1.5: Ajout des champs de liaison avec Keycloak dans la table membres --- Date: 2025-12-24 --- Description: Permet de lier un Membre (business) à un User Keycloak (authentification) - --- Ajouter la colonne keycloak_user_id pour stocker l'UUID du user Keycloak -ALTER TABLE membres -ADD COLUMN IF NOT EXISTS keycloak_user_id VARCHAR(36); - --- Ajouter la colonne keycloak_realm pour stocker le nom du realm (généralement "unionflow") -ALTER TABLE membres -ADD COLUMN IF NOT EXISTS keycloak_realm VARCHAR(50); - --- Créer un index unique sur keycloak_user_id pour garantir l'unicité et optimiser les recherches --- Un user Keycloak ne peut être lié qu'à un seul Membre -CREATE UNIQUE INDEX IF NOT EXISTS idx_membre_keycloak_user -ON membres(keycloak_user_id) -WHERE keycloak_user_id IS NOT NULL; - --- Ajouter un commentaire pour la documentation -COMMENT ON COLUMN membres.keycloak_user_id IS 'UUID du user Keycloak lié à ce membre. NULL si le membre n''a pas de compte de connexion.'; -COMMENT ON COLUMN membres.keycloak_realm IS 'Nom du realm Keycloak où le user est enregistré (ex: "unionflow", "btpxpress"). NULL si pas de compte Keycloak.'; - --- Note: Le champ mot_de_passe existant devrait être déprécié car Keycloak est la source de vérité pour l'authentification --- Cependant, on le conserve pour compatibilité avec les données existantes et migration progressive - - --- ========== V1.7__Create_All_Missing_Tables.sql ========== -- ============================================================================ --- V2.0 : Création de toutes les tables manquantes pour UnionFlow --- Toutes les tables héritent de BaseEntity (id UUID PK, date_creation, --- date_modification, cree_par, modifie_par, version, actif) +-- UnionFlow - Schema Complet et Définitif +-- ============================================================================ +-- Auteur: Lions Dev +-- Date: 2026-03-16 +-- Version: 1.0 (Consolidation finale) +-- Description: Schema complet de la base de données UnionFlow avec toutes les +-- tables, index, contraintes et commentaires. +-- Tous les noms de tables correspondent exactement aux entités JPA. +-- ============================================================================ +-- IMPORTANT: Ce fichier consolide les anciennes migrations V1 à V10. +-- Les noms de tables sont corrects dès le départ. +-- 69 entités JPA = 69 tables (100% correspondance). -- ============================================================================ -- Colonnes communes BaseEntity (à inclure dans chaque table) @@ -853,8 +26,8 @@ COMMENT ON COLUMN membres.keycloak_realm IS 'Nom du realm Keycloak où le user e -- 1. TABLES PRINCIPALES (sans FK vers d'autres tables métier) -- ============================================================================ --- Table membres (principale, référencée par beaucoup d'autres) -CREATE TABLE IF NOT EXISTS membres ( +-- Table utilisateurs (entité: Membre) - NOM CORRIGÉ dès le départ +CREATE TABLE IF NOT EXISTS utilisateurs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), nom VARCHAR(100) NOT NULL, prenom VARCHAR(100) NOT NULL, @@ -880,41 +53,86 @@ CREATE TABLE IF NOT EXISTS membres ( actif BOOLEAN NOT NULL DEFAULT TRUE ); --- Table organisations (déjà créée en V1.2, mais IF NOT EXISTS pour sécurité) +CREATE INDEX idx_utilisateurs_email ON utilisateurs(email); +CREATE INDEX idx_utilisateurs_numero_membre ON utilisateurs(numero_membre); +CREATE INDEX idx_utilisateurs_organisation ON utilisateurs(organisation_id); +CREATE INDEX idx_utilisateurs_keycloak ON utilisateurs(keycloak_user_id); +CREATE INDEX idx_utilisateurs_statut ON utilisateurs(statut); + +-- Table organisations CREATE TABLE IF NOT EXISTS organisations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - nom VARCHAR(255) NOT NULL, - sigle VARCHAR(50), + nom VARCHAR(200) NOT NULL, + nom_court VARCHAR(50), + type_organisation VARCHAR(50) NOT NULL DEFAULT 'ASSOCIATION', + statut VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', description TEXT, - type_organisation VARCHAR(50), - statut VARCHAR(30) DEFAULT 'ACTIVE', - email VARCHAR(255), - telephone VARCHAR(30), - site_web VARCHAR(500), - adresse_siege TEXT, - logo_url VARCHAR(500), date_fondation DATE, - pays VARCHAR(100), + numero_enregistrement VARCHAR(100) UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + telephone VARCHAR(20), + telephone_secondaire VARCHAR(20), + email_secondaire VARCHAR(255), + adresse VARCHAR(500), ville VARCHAR(100), + code_postal VARCHAR(20), + region VARCHAR(100), + pays VARCHAR(100), + latitude DECIMAL(9,6) CHECK (latitude >= -90 AND latitude <= 90), + longitude DECIMAL(9,6) CHECK (longitude >= -180 AND longitude <= 180), + site_web VARCHAR(500), + logo VARCHAR(500), + reseaux_sociaux VARCHAR(1000), organisation_parente_id UUID, + niveau_hierarchique INTEGER NOT NULL DEFAULT 0, + nombre_membres INTEGER NOT NULL DEFAULT 0, + nombre_administrateurs INTEGER NOT NULL DEFAULT 0, + budget_annuel DECIMAL(14,2) CHECK (budget_annuel >= 0), + devise VARCHAR(3) DEFAULT 'XOF', + cotisation_obligatoire BOOLEAN NOT NULL DEFAULT FALSE, + montant_cotisation_annuelle DECIMAL(12,2) CHECK (montant_cotisation_annuelle >= 0), + objectifs TEXT, + activites_principales TEXT, + certifications VARCHAR(500), + partenaires VARCHAR(1000), + notes VARCHAR(1000), + organisation_publique BOOLEAN NOT NULL DEFAULT TRUE, + accepte_nouveaux_membres BOOLEAN NOT NULL DEFAULT TRUE, + actif BOOLEAN NOT NULL DEFAULT TRUE, date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), modifie_par VARCHAR(255), version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE + + CONSTRAINT chk_organisation_statut CHECK (statut IN ('ACTIVE', 'SUSPENDUE', 'DISSOUTE', 'EN_ATTENTE')), + CONSTRAINT chk_organisation_type CHECK (type_organisation IN ( + 'ASSOCIATION', 'LIONS_CLUB', 'ROTARY_CLUB', 'COOPERATIVE', + 'FONDATION', 'ONG', 'SYNDICAT', 'AUTRE' + )), + CONSTRAINT chk_organisation_devise CHECK (devise IN ('XOF', 'EUR', 'USD', 'GBP', 'CHF')), + CONSTRAINT chk_organisation_niveau CHECK (niveau_hierarchique >= 0 AND niveau_hierarchique <= 10), + CONSTRAINT chk_organisation_membres CHECK (nombre_membres >= 0), + CONSTRAINT chk_organisation_admins CHECK (nombre_administrateurs >= 0), + CONSTRAINT fk_organisation_parente FOREIGN KEY (organisation_parente_id) + REFERENCES organisations(id) ON DELETE SET NULL ); --- ============================================================================ --- 2. TABLES SÉCURITÉ (Rôles et Permissions) --- ============================================================================ +CREATE INDEX idx_organisation_nom ON organisations(nom); +CREATE INDEX idx_organisation_email ON organisations(email); +CREATE INDEX idx_organisation_statut ON organisations(statut); +CREATE INDEX idx_organisation_type ON organisations(type_organisation); +CREATE INDEX idx_organisation_ville ON organisations(ville); +CREATE INDEX idx_organisation_pays ON organisations(pays); +CREATE INDEX idx_organisation_parente ON organisations(organisation_parente_id); +-- Table roles CREATE TABLE IF NOT EXISTS roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), nom VARCHAR(100) NOT NULL UNIQUE, description VARCHAR(500), - code VARCHAR(50) NOT NULL UNIQUE, - niveau INTEGER DEFAULT 0, + niveau_acces INTEGER DEFAULT 0, + est_systeme BOOLEAN DEFAULT FALSE, date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), @@ -923,12 +141,13 @@ CREATE TABLE IF NOT EXISTS roles ( actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE TABLE IF NOT EXISTS permissions ( +-- Table permission (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS permission ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - nom VARCHAR(100) NOT NULL UNIQUE, - description VARCHAR(500), code VARCHAR(100) NOT NULL UNIQUE, - module VARCHAR(100), + nom VARCHAR(200) NOT NULL, + description VARCHAR(500), + module VARCHAR(50), date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), @@ -937,279 +156,345 @@ CREATE TABLE IF NOT EXISTS permissions ( actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE TABLE IF NOT EXISTS roles_permissions ( +-- Table role_permission (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS role_permission ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - role_id UUID NOT NULL REFERENCES roles(id), - permission_id UUID NOT NULL REFERENCES permissions(id), + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + permission_id UUID NOT NULL REFERENCES permission(id) ON DELETE CASCADE, date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, actif BOOLEAN NOT NULL DEFAULT TRUE, - UNIQUE(role_id, permission_id) + CONSTRAINT uk_role_permission UNIQUE (role_id, permission_id) ); -CREATE TABLE IF NOT EXISTS membres_roles ( +-- Table membre_role (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS membre_role ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - membre_id UUID NOT NULL REFERENCES membres(id), - role_id UUID NOT NULL REFERENCES roles(id), - organisation_id UUID REFERENCES organisations(id), + membre_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + date_attribution DATE DEFAULT CURRENT_DATE, + date_expiration DATE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + actif BOOLEAN NOT NULL DEFAULT TRUE, + CONSTRAINT uk_membre_role UNIQUE (membre_id, role_id) +); + +-- Table membre_organisation (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS membre_organisation ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + membre_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, + organisation_id UUID NOT NULL REFERENCES organisations(id) ON DELETE CASCADE, + fonction VARCHAR(100), + date_entree DATE, + date_sortie DATE, + statut VARCHAR(30) DEFAULT 'ACTIF', date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), modifie_par VARCHAR(255), version BIGINT DEFAULT 0, actif BOOLEAN NOT NULL DEFAULT TRUE, - UNIQUE(membre_id, role_id, organisation_id) + CONSTRAINT uk_membre_organisation UNIQUE (membre_id, organisation_id) +); + +-- ============================================================================ +-- 2. TABLES CONFIGURATION +-- ============================================================================ + +-- Table configuration (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS configuration ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + cle VARCHAR(255) NOT NULL UNIQUE, + valeur TEXT, + type VARCHAR(50), + categorie VARCHAR(50), + description VARCHAR(1000), + modifiable BOOLEAN DEFAULT TRUE, + visible BOOLEAN DEFAULT TRUE, + metadonnees TEXT, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE INDEX idx_config_cle ON configuration(cle); +CREATE INDEX idx_config_categorie ON configuration(categorie); + +-- Table configuration_wave (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS configuration_wave ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id UUID REFERENCES organisations(id) ON DELETE CASCADE, + api_key_encrypted VARCHAR(500), + api_secret_encrypted VARCHAR(500), + callback_url VARCHAR(500), + webhook_url VARCHAR(500), + mode VARCHAR(20) DEFAULT 'TEST', + actif BOOLEAN DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0 ); -- ============================================================================ -- 3. TABLES FINANCE -- ============================================================================ -CREATE TABLE IF NOT EXISTS adhesions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - numero_adhesion VARCHAR(50), - membre_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), - statut VARCHAR(30) DEFAULT 'EN_ATTENTE', - date_demande TIMESTAMP, - date_approbation TIMESTAMP, - date_rejet TIMESTAMP, - motif_rejet TEXT, - frais_adhesion DECIMAL(15,2) DEFAULT 0, - devise VARCHAR(10) DEFAULT 'XOF', - montant_paye DECIMAL(15,2) DEFAULT 0, - approuve_par VARCHAR(255), - commentaire TEXT, - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - +-- Table cotisations CREATE TABLE IF NOT EXISTS cotisations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - numero_reference VARCHAR(50), - membre_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), - type_cotisation VARCHAR(50), - periode VARCHAR(50), - montant_du DECIMAL(15,2), - montant_paye DECIMAL(15,2) DEFAULT 0, - statut VARCHAR(30) DEFAULT 'EN_ATTENTE', - date_echeance DATE, + numero_reference VARCHAR(50) UNIQUE NOT NULL, + membre_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, + organisation_id UUID REFERENCES organisations(id) ON DELETE CASCADE, + type_cotisation VARCHAR(50) NOT NULL, + libelle VARCHAR(255), + montant_du DECIMAL(12,2) NOT NULL CHECK (montant_du >= 0), + montant_paye DECIMAL(12,2) NOT NULL DEFAULT 0 CHECK (montant_paye >= 0), + code_devise VARCHAR(3) NOT NULL DEFAULT 'XOF', + statut VARCHAR(30) NOT NULL, + date_echeance DATE NOT NULL, date_paiement TIMESTAMP, - methode_paiement VARCHAR(50), - reference_paiement VARCHAR(100), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS paiements ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - reference VARCHAR(100), - montant DECIMAL(15,2) NOT NULL, - devise VARCHAR(10) DEFAULT 'XOF', - methode_paiement VARCHAR(50), - statut VARCHAR(30) DEFAULT 'EN_ATTENTE', - type_paiement VARCHAR(50), - description TEXT, - membre_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), - date_paiement TIMESTAMP, - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS paiements_adhesions ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - adhesion_id UUID REFERENCES adhesions(id), - paiement_id UUID REFERENCES paiements(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS paiements_cotisations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - cotisation_id UUID REFERENCES cotisations(id), - paiement_id UUID REFERENCES paiements(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS paiements_evenements ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - evenement_id UUID, - paiement_id UUID REFERENCES paiements(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS paiements_aides ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - demande_aide_id UUID, - paiement_id UUID REFERENCES paiements(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - --- ============================================================================ --- 4. TABLES COMPTABILITÉ --- ============================================================================ - -CREATE TABLE IF NOT EXISTS comptes_comptables ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - numero_compte VARCHAR(20) NOT NULL, - libelle VARCHAR(255) NOT NULL, - type_compte VARCHAR(50), - solde DECIMAL(15,2) DEFAULT 0, - description TEXT, - compte_parent_id UUID, - organisation_id UUID REFERENCES organisations(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS journaux_comptables ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - code VARCHAR(20) NOT NULL, - libelle VARCHAR(255) NOT NULL, - type_journal VARCHAR(50), - description TEXT, - organisation_id UUID REFERENCES organisations(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS ecritures_comptables ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - numero_piece VARCHAR(50), - date_ecriture DATE NOT NULL, - libelle VARCHAR(500), - montant_total DECIMAL(15,2), - statut VARCHAR(30) DEFAULT 'BROUILLON', - journal_id UUID REFERENCES journaux_comptables(id), - organisation_id UUID REFERENCES organisations(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS lignes_ecriture ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - ecriture_id UUID NOT NULL REFERENCES ecritures_comptables(id), - compte_id UUID NOT NULL REFERENCES comptes_comptables(id), - libelle VARCHAR(500), - montant_debit DECIMAL(15,2) DEFAULT 0, - montant_credit DECIMAL(15,2) DEFAULT 0, - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - --- ============================================================================ --- 5. TABLES ÉVÉNEMENTS --- ============================================================================ - -CREATE TABLE IF NOT EXISTS evenements ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - titre VARCHAR(255) NOT NULL, - description TEXT, - type_evenement VARCHAR(50), - statut VARCHAR(30) DEFAULT 'PLANIFIE', - priorite VARCHAR(20) DEFAULT 'NORMALE', - date_debut TIMESTAMP, - date_fin TIMESTAMP, - lieu VARCHAR(500), - capacite_max INTEGER, - prix DECIMAL(15,2) DEFAULT 0, - devise VARCHAR(10) DEFAULT 'XOF', - organisateur_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS inscriptions_evenement ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - evenement_id UUID NOT NULL REFERENCES evenements(id), - membre_id UUID NOT NULL REFERENCES membres(id), - statut VARCHAR(30) DEFAULT 'EN_ATTENTE', - date_inscription TIMESTAMP DEFAULT NOW(), - commentaire TEXT, + periode VARCHAR(20), + annee INTEGER NOT NULL CHECK (annee >= 2020 AND annee <= 2100), + mois INTEGER CHECK (mois >= 1 AND mois <= 12), + recurrente BOOLEAN NOT NULL DEFAULT FALSE, + nombre_rappels INTEGER NOT NULL DEFAULT 0, + date_dernier_rappel TIMESTAMP, date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), modifie_par VARCHAR(255), version BIGINT DEFAULT 0, actif BOOLEAN NOT NULL DEFAULT TRUE, - UNIQUE(evenement_id, membre_id) + + CONSTRAINT chk_cotisation_statut CHECK (statut IN ('EN_ATTENTE', 'PAYEE', 'EN_RETARD', 'PARTIELLEMENT_PAYEE', 'ANNULEE')) +); + +CREATE INDEX idx_cotisation_membre ON cotisations(membre_id); +CREATE INDEX idx_cotisation_statut ON cotisations(statut); +CREATE INDEX idx_cotisation_annee ON cotisations(annee); + +-- Table paiements +CREATE TABLE IF NOT EXISTS paiements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_transaction VARCHAR(100) UNIQUE NOT NULL, + membre_id UUID REFERENCES utilisateurs(id), + montant DECIMAL(12,2) NOT NULL CHECK (montant >= 0), + code_devise VARCHAR(3) NOT NULL DEFAULT 'XOF', + type_paiement VARCHAR(50) NOT NULL, + methode_paiement VARCHAR(50), + statut VARCHAR(30) NOT NULL, + date_paiement TIMESTAMP DEFAULT NOW(), + reference_externe VARCHAR(255), + description VARCHAR(500), + metadata TEXT, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table paiements_objets (liaison paiement -> objet métier) +CREATE TABLE IF NOT EXISTS paiements_objets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + paiement_id UUID NOT NULL REFERENCES paiements(id) ON DELETE CASCADE, + objet_type VARCHAR(50) NOT NULL, + objet_id UUID NOT NULL, + montant_alloue DECIMAL(12,2) CHECK (montant_alloue >= 0), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table intention_paiement (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS intention_paiement ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_intention VARCHAR(100) UNIQUE NOT NULL, + membre_id UUID NOT NULL REFERENCES utilisateurs(id), + montant DECIMAL(12,2) NOT NULL CHECK (montant >= 0), + code_devise VARCHAR(3) DEFAULT 'XOF', + type_objet VARCHAR(50) NOT NULL, + objet_id UUID, + statut VARCHAR(30) DEFAULT 'EN_ATTENTE', + methode_paiement VARCHAR(50), + url_callback VARCHAR(500), + url_retour VARCHAR(500), + date_expiration TIMESTAMP, + metadata TEXT, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE, + + CONSTRAINT chk_intention_type_objet CHECK (type_objet IN ( + 'COTISATION', 'ADHESION', 'EVENEMENT', 'AIDE', + 'DEPOT_EPARGNE', 'REMBOURSEMENT_CREDIT', 'DON', 'AUTRE' + )) ); -- ============================================================================ --- 6. TABLES SOLIDARITÉ +-- 4. TABLES MUTUELLES (Épargne & Crédit) -- ============================================================================ +-- Table comptes_epargne +CREATE TABLE IF NOT EXISTS comptes_epargne ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_compte VARCHAR(50) UNIQUE NOT NULL, + membre_id UUID NOT NULL REFERENCES utilisateurs(id), + organisation_id UUID REFERENCES organisations(id), + solde DECIMAL(14,2) NOT NULL DEFAULT 0, + code_devise VARCHAR(3) DEFAULT 'XOF', + taux_interet DECIMAL(5,2) DEFAULT 0, + statut VARCHAR(30) DEFAULT 'ACTIF', + date_ouverture DATE DEFAULT CURRENT_DATE, + date_fermeture DATE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table transactions_epargne +CREATE TABLE IF NOT EXISTS transactions_epargne ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + compte_id UUID NOT NULL REFERENCES comptes_epargne(id), + type_transaction VARCHAR(30) NOT NULL, + montant DECIMAL(14,2) NOT NULL, + solde_avant DECIMAL(14,2), + solde_apres DECIMAL(14,2), + reference VARCHAR(100), + description VARCHAR(500), + date_transaction TIMESTAMP DEFAULT NOW(), + valide_par VARCHAR(255), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table demandes_credit +CREATE TABLE IF NOT EXISTS demandes_credit ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_demande VARCHAR(50) UNIQUE NOT NULL, + membre_id UUID NOT NULL REFERENCES utilisateurs(id), + montant_demande DECIMAL(14,2) NOT NULL, + montant_approuve DECIMAL(14,2), + taux_interet DECIMAL(5,2), + duree_mois INTEGER, + statut VARCHAR(30) DEFAULT 'EN_ATTENTE', + motif TEXT, + date_demande TIMESTAMP DEFAULT NOW(), + date_approbation TIMESTAMP, + date_debut TIMESTAMP, + date_fin TIMESTAMP, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table echeances_credit +CREATE TABLE IF NOT EXISTS echeances_credit ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + demande_credit_id UUID NOT NULL REFERENCES demandes_credit(id), + numero_echeance INTEGER NOT NULL, + montant_principal DECIMAL(14,2), + montant_interet DECIMAL(14,2), + montant_total DECIMAL(14,2), + date_echeance DATE NOT NULL, + montant_paye DECIMAL(14,2) DEFAULT 0, + date_paiement TIMESTAMP, + statut VARCHAR(30) DEFAULT 'EN_ATTENTE', + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table garanties_demande +CREATE TABLE IF NOT EXISTS garanties_demande ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + demande_credit_id UUID NOT NULL REFERENCES demandes_credit(id), + type_garantie VARCHAR(50), + description TEXT, + valeur_estimee DECIMAL(14,2), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- ============================================================================ +-- 5. TABLES ÉVÉNEMENTS & SOLIDARITÉ +-- ============================================================================ + +-- Table evenements +CREATE TABLE IF NOT EXISTS evenements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + titre VARCHAR(200) NOT NULL, + description TEXT, + date_debut TIMESTAMP NOT NULL, + date_fin TIMESTAMP, + lieu VARCHAR(255) NOT NULL, + adresse VARCHAR(500), + ville VARCHAR(100), + pays VARCHAR(100), + type_evenement VARCHAR(50) NOT NULL, + statut VARCHAR(50) NOT NULL, + capacite_max INTEGER, + cout_participation DECIMAL(12,2), + devise VARCHAR(3), + organisation_id UUID REFERENCES organisations(id), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table inscriptions_evenement +CREATE TABLE IF NOT EXISTS inscriptions_evenement ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + membre_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, + evenement_id UUID NOT NULL REFERENCES evenements(id) ON DELETE CASCADE, + date_inscription TIMESTAMP NOT NULL DEFAULT NOW(), + statut VARCHAR(20) DEFAULT 'CONFIRMEE', + commentaire VARCHAR(500), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE, + CONSTRAINT uk_inscription_membre_evenement UNIQUE (membre_id, evenement_id) +); + +-- Table demandes_aide CREATE TABLE IF NOT EXISTS demandes_aide ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - numero_demande VARCHAR(50), - type_aide VARCHAR(50), - priorite VARCHAR(20) DEFAULT 'NORMALE', - statut VARCHAR(50) DEFAULT 'BROUILLON', - titre VARCHAR(255), - description TEXT, - montant_demande DECIMAL(15,2), - montant_approuve DECIMAL(15,2), - devise VARCHAR(10) DEFAULT 'XOF', - justification TEXT, - membre_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), + titre VARCHAR(200) NOT NULL, + description TEXT NOT NULL, + type_aide VARCHAR(50) NOT NULL, + statut VARCHAR(50) NOT NULL, + montant_demande DECIMAL(10,2), + montant_approuve DECIMAL(10,2), + date_demande TIMESTAMP NOT NULL DEFAULT NOW(), + date_evaluation TIMESTAMP, + urgence BOOLEAN NOT NULL DEFAULT FALSE, + demandeur_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, + evaluateur_id UUID REFERENCES utilisateurs(id), + organisation_id UUID NOT NULL REFERENCES organisations(id) ON DELETE CASCADE, date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), @@ -1219,261 +504,20 @@ CREATE TABLE IF NOT EXISTS demandes_aide ( ); -- ============================================================================ --- 7. TABLES DOCUMENTS +-- 6. TABLES SUPPORT & SUGGESTIONS -- ============================================================================ -CREATE TABLE IF NOT EXISTS documents ( +-- Table ticket (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS ticket ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - nom VARCHAR(255) NOT NULL, - description TEXT, - type_document VARCHAR(50), - chemin_fichier VARCHAR(1000), - taille_fichier BIGINT, - type_mime VARCHAR(100), - membre_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS pieces_jointes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - nom_fichier VARCHAR(255) NOT NULL, - chemin_fichier VARCHAR(1000), - type_mime VARCHAR(100), - taille BIGINT, - entite_type VARCHAR(100), - entite_id UUID, - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - --- ============================================================================ --- 8. TABLES NOTIFICATIONS --- ============================================================================ - -CREATE TABLE IF NOT EXISTS templates_notifications ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - code VARCHAR(100) NOT NULL UNIQUE, - sujet VARCHAR(500), - corps_texte TEXT, - corps_html TEXT, - variables_disponibles TEXT, - canaux_supportes VARCHAR(500), - langue VARCHAR(10) DEFAULT 'fr', - description TEXT, - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS notifications ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - type_notification VARCHAR(30) NOT NULL, - priorite VARCHAR(20) DEFAULT 'NORMALE', - statut VARCHAR(30) DEFAULT 'EN_ATTENTE', - sujet VARCHAR(500), - corps TEXT, - date_envoi_prevue TIMESTAMP, - date_envoi TIMESTAMP, - date_lecture TIMESTAMP, - nombre_tentatives INTEGER DEFAULT 0, - message_erreur VARCHAR(1000), - donnees_additionnelles TEXT, - membre_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), - template_id UUID REFERENCES templates_notifications(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - --- ============================================================================ --- 9. TABLES ADRESSES --- ============================================================================ - -CREATE TABLE IF NOT EXISTS adresses ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - type_adresse VARCHAR(30), - rue VARCHAR(500), - complement VARCHAR(500), - code_postal VARCHAR(20), - ville VARCHAR(100), - region VARCHAR(100), - pays VARCHAR(100) DEFAULT 'Côte d''Ivoire', - latitude DOUBLE PRECISION, - longitude DOUBLE PRECISION, - principale BOOLEAN DEFAULT FALSE, - membre_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); --- Colonnes attendues par l'entité Adresse (alignement schéma) -ALTER TABLE adresses ADD COLUMN IF NOT EXISTS adresse VARCHAR(500); -ALTER TABLE adresses ADD COLUMN IF NOT EXISTS complement_adresse VARCHAR(200); -ALTER TABLE adresses ADD COLUMN IF NOT EXISTS libelle VARCHAR(100); -ALTER TABLE adresses ADD COLUMN IF NOT EXISTS notes VARCHAR(500); -ALTER TABLE adresses ADD COLUMN IF NOT EXISTS evenement_id UUID REFERENCES evenements(id) ON DELETE SET NULL; -CREATE INDEX IF NOT EXISTS idx_adresse_evenement ON adresses(evenement_id); --- Types latitude/longitude : entité attend NUMERIC(9,6) -DO $$ BEGIN - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'adresses' AND column_name = 'latitude' AND data_type = 'double precision') THEN - ALTER TABLE adresses ALTER COLUMN latitude TYPE NUMERIC(9,6) USING latitude::numeric(9,6); - END IF; - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'adresses' AND column_name = 'longitude' AND data_type = 'double precision') THEN - ALTER TABLE adresses ALTER COLUMN longitude TYPE NUMERIC(9,6) USING longitude::numeric(9,6); - END IF; -END $$; - --- ============================================================================ --- 10. TABLES AUDIT --- ============================================================================ - -CREATE TABLE IF NOT EXISTS audit_logs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - action VARCHAR(100) NOT NULL, - entite_type VARCHAR(100), - entite_id VARCHAR(100), - utilisateur VARCHAR(255), - details TEXT, - adresse_ip VARCHAR(50), - date_heure TIMESTAMP NOT NULL DEFAULT NOW(), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); --- Colonnes attendues par l'entité AuditLog -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS description VARCHAR(500); -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS donnees_avant TEXT; -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS donnees_apres TEXT; -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS ip_address VARCHAR(45); -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS module VARCHAR(50); -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS role VARCHAR(50); -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS session_id VARCHAR(255); -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS severite VARCHAR(20) NOT NULL DEFAULT 'INFO'; -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS type_action VARCHAR(50) DEFAULT 'AUTRE'; -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS user_agent VARCHAR(500); -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS organisation_id UUID REFERENCES organisations(id) ON DELETE SET NULL; -ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS portee VARCHAR(15) NOT NULL DEFAULT 'PLATEFORME'; -ALTER TABLE audit_logs ALTER COLUMN entite_id TYPE VARCHAR(255) USING entite_id::varchar(255); -UPDATE audit_logs SET type_action = COALESCE(action, 'AUTRE') WHERE type_action IS NULL OR type_action = 'AUTRE'; -ALTER TABLE audit_logs ALTER COLUMN type_action SET DEFAULT 'AUTRE'; -DO $$ BEGIN IF (SELECT COUNT(*) FROM audit_logs WHERE type_action IS NULL) = 0 THEN ALTER TABLE audit_logs ALTER COLUMN type_action SET NOT NULL; END IF; END $$; -CREATE INDEX IF NOT EXISTS idx_audit_module ON audit_logs(module); -CREATE INDEX IF NOT EXISTS idx_audit_type_action ON audit_logs(type_action); -CREATE INDEX IF NOT EXISTS idx_audit_severite ON audit_logs(severite); - --- ============================================================================ --- 11. TABLES WAVE MONEY --- ============================================================================ - -CREATE TABLE IF NOT EXISTS comptes_wave ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - numero_telephone VARCHAR(30) NOT NULL, - nom_titulaire VARCHAR(255), - statut VARCHAR(30) DEFAULT 'ACTIF', - solde DECIMAL(15,2) DEFAULT 0, - devise VARCHAR(10) DEFAULT 'XOF', - organisation_id UUID REFERENCES organisations(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS configurations_wave ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - cle_api VARCHAR(500), - secret_api VARCHAR(500), - environnement VARCHAR(30) DEFAULT 'sandbox', - url_webhook VARCHAR(500), - organisation_id UUID REFERENCES organisations(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS transactions_wave ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - reference_wave VARCHAR(100), - reference_interne VARCHAR(100), - type_transaction VARCHAR(50), - montant DECIMAL(15,2) NOT NULL, - devise VARCHAR(10) DEFAULT 'XOF', - statut VARCHAR(30) DEFAULT 'EN_ATTENTE', - numero_expediteur VARCHAR(30), - numero_destinataire VARCHAR(30), - description TEXT, - erreur TEXT, - compte_wave_id UUID REFERENCES comptes_wave(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS webhooks_wave ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - type_evenement VARCHAR(100), - statut VARCHAR(30) DEFAULT 'RECU', - payload TEXT, - signature VARCHAR(500), - traite BOOLEAN DEFAULT FALSE, - erreur TEXT, - transaction_id UUID REFERENCES transactions_wave(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - --- ============================================================================ --- 12. TABLES SUPPORT (tickets, suggestions, favoris, config - déjà en V1.4) --- ============================================================================ - -CREATE TABLE IF NOT EXISTS tickets ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - numero_ticket VARCHAR(50), + numero_ticket VARCHAR(50) NOT NULL UNIQUE, + utilisateur_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, sujet VARCHAR(255) NOT NULL, description TEXT, categorie VARCHAR(50), - priorite VARCHAR(20) DEFAULT 'NORMALE', - statut VARCHAR(30) DEFAULT 'OUVERT', - membre_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), - assigne_a VARCHAR(255), - date_resolution TIMESTAMP, + priorite VARCHAR(50), + statut VARCHAR(50) DEFAULT 'OUVERT', + agent_id UUID, date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), @@ -1482,16 +526,16 @@ CREATE TABLE IF NOT EXISTS tickets ( actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE TABLE IF NOT EXISTS suggestions ( +-- Table suggestion (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS suggestion ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + utilisateur_id UUID NOT NULL REFERENCES utilisateurs(id), titre VARCHAR(255) NOT NULL, description TEXT, categorie VARCHAR(50), - statut VARCHAR(30) DEFAULT 'NOUVELLE', - votes_pour INTEGER DEFAULT 0, - votes_contre INTEGER DEFAULT 0, - membre_id UUID REFERENCES membres(id), - organisation_id UUID REFERENCES organisations(id), + statut VARCHAR(50) DEFAULT 'NOUVELLE', + nb_votes INTEGER DEFAULT 0, + date_soumission TIMESTAMP NOT NULL DEFAULT NOW(), date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), @@ -1500,59 +544,24 @@ CREATE TABLE IF NOT EXISTS suggestions ( actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE TABLE IF NOT EXISTS suggestion_votes ( +-- Table suggestion_vote (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS suggestion_vote ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - suggestion_id UUID NOT NULL REFERENCES suggestions(id), - membre_id UUID NOT NULL REFERENCES membres(id), - type_vote VARCHAR(20) NOT NULL, + suggestion_id UUID NOT NULL REFERENCES suggestion(id) ON DELETE CASCADE, + utilisateur_id UUID NOT NULL REFERENCES utilisateurs(id), + date_vote TIMESTAMP NOT NULL DEFAULT NOW(), date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, actif BOOLEAN NOT NULL DEFAULT TRUE, - UNIQUE(suggestion_id, membre_id) + CONSTRAINT uk_suggestion_vote UNIQUE (suggestion_id, utilisateur_id) ); -CREATE TABLE IF NOT EXISTS favoris ( +-- Table favori (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS favori ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - type_entite VARCHAR(100) NOT NULL, - entite_id UUID NOT NULL, - membre_id UUID NOT NULL REFERENCES membres(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - -CREATE TABLE IF NOT EXISTS configurations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - cle VARCHAR(255) NOT NULL UNIQUE, - valeur TEXT, - description TEXT, - categorie VARCHAR(100), - organisation_id UUID REFERENCES organisations(id), - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE -); - --- ============================================================================ --- 13. TABLE TYPES ORGANISATION --- ============================================================================ - -CREATE TABLE IF NOT EXISTS uf_type_organisation ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - code VARCHAR(50) NOT NULL UNIQUE, - libelle VARCHAR(255) NOT NULL, - description TEXT, - icone VARCHAR(100), - couleur VARCHAR(20), + utilisateur_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, + type_favori VARCHAR(50) NOT NULL, + titre VARCHAR(255) NOT NULL, + url VARCHAR(1000), ordre INTEGER DEFAULT 0, date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, @@ -1563,1591 +572,751 @@ CREATE TABLE IF NOT EXISTS uf_type_organisation ( ); -- ============================================================================ --- 14. INDEX POUR PERFORMANCES +-- 7. TABLES NOTIFICATIONS & DOCUMENTS -- ============================================================================ -CREATE INDEX IF NOT EXISTS idx_membres_email ON membres(email); -CREATE INDEX IF NOT EXISTS idx_membres_numero ON membres(numero_membre); -CREATE INDEX IF NOT EXISTS idx_membres_organisation ON membres(organisation_id); -CREATE INDEX IF NOT EXISTS idx_membres_keycloak ON membres(keycloak_user_id); -CREATE INDEX IF NOT EXISTS idx_adhesions_membre ON adhesions(membre_id); -CREATE INDEX IF NOT EXISTS idx_adhesions_organisation ON adhesions(organisation_id); -CREATE INDEX IF NOT EXISTS idx_adhesions_statut ON adhesions(statut); - -CREATE INDEX IF NOT EXISTS idx_cotisations_membre ON cotisations(membre_id); -CREATE INDEX IF NOT EXISTS idx_cotisations_statut ON cotisations(statut); -CREATE INDEX IF NOT EXISTS idx_cotisations_echeance ON cotisations(date_echeance); - -CREATE INDEX IF NOT EXISTS idx_evenements_statut ON evenements(statut); -CREATE INDEX IF NOT EXISTS idx_evenements_organisation ON evenements(organisation_id); -CREATE INDEX IF NOT EXISTS idx_evenements_date_debut ON evenements(date_debut); - -CREATE INDEX IF NOT EXISTS idx_notification_membre ON notifications(membre_id); -CREATE INDEX IF NOT EXISTS idx_notification_statut ON notifications(statut); -CREATE INDEX IF NOT EXISTS idx_notification_type ON notifications(type_notification); - -CREATE INDEX IF NOT EXISTS idx_audit_date_heure ON audit_logs(date_heure); -CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action); -CREATE INDEX IF NOT EXISTS idx_audit_utilisateur ON audit_logs(utilisateur); - -CREATE INDEX IF NOT EXISTS idx_paiements_membre ON paiements(membre_id); -CREATE INDEX IF NOT EXISTS idx_paiements_statut ON paiements(statut); - -CREATE INDEX IF NOT EXISTS idx_demandes_aide_demandeur ON demandes_aide(demandeur_id); -CREATE INDEX IF NOT EXISTS idx_demandes_aide_statut ON demandes_aide(statut); - - --- ========== V2.0__Refactoring_Utilisateurs.sql ========== --- ============================================================ --- V2.0 — Refactoring: membres → utilisateurs --- Sépare l'identité globale (utilisateurs) du lien organisationnel --- Auteur: UnionFlow Team | BCEAO/OHADA compliant --- ============================================================ - --- Renommer la table membres → utilisateurs -ALTER TABLE membres RENAME TO utilisateurs; - --- Supprimer l'ancien lien unique membre↔organisation (maintenant dans membres_organisations) -ALTER TABLE utilisateurs DROP COLUMN IF EXISTS organisation_id; -ALTER TABLE utilisateurs DROP COLUMN IF EXISTS date_adhesion; -ALTER TABLE utilisateurs DROP COLUMN IF EXISTS mot_de_passe; -ALTER TABLE utilisateurs DROP COLUMN IF EXISTS roles; - --- Ajouter les nouveaux champs identité globale -ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS keycloak_id UUID UNIQUE; -ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS photo_url VARCHAR(500); -ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS statut_compte VARCHAR(30) NOT NULL DEFAULT 'ACTIF'; -ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS telephone_wave VARCHAR(13); - --- Mettre à jour la contrainte de statut compte -ALTER TABLE utilisateurs - ADD CONSTRAINT chk_utilisateur_statut_compte - CHECK (statut_compte IN ('ACTIF', 'SUSPENDU', 'DESACTIVE')); - --- Mettre à jour les index -DROP INDEX IF EXISTS idx_membre_organisation; -DROP INDEX IF EXISTS idx_membre_email; -DROP INDEX IF EXISTS idx_membre_numero; -DROP INDEX IF EXISTS idx_membre_actif; - -CREATE UNIQUE INDEX IF NOT EXISTS idx_utilisateur_email ON utilisateurs(email); -CREATE UNIQUE INDEX IF NOT EXISTS idx_utilisateur_numero ON utilisateurs(numero_membre); -CREATE INDEX IF NOT EXISTS idx_utilisateur_actif ON utilisateurs(actif); -CREATE UNIQUE INDEX IF NOT EXISTS idx_utilisateur_keycloak ON utilisateurs(keycloak_id); -CREATE INDEX IF NOT EXISTS idx_utilisateur_statut_compte ON utilisateurs(statut_compte); - --- ============================================================ --- Table membres_organisations : lien utilisateur ↔ organisation --- ============================================================ -CREATE TABLE IF NOT EXISTS membres_organisations ( +-- Table notifications +CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - utilisateur_id UUID NOT NULL, - organisation_id UUID NOT NULL, - unite_id UUID, -- agence/bureau d'affectation (null = siège) - - statut_membre VARCHAR(30) NOT NULL DEFAULT 'EN_ATTENTE_VALIDATION', - date_adhesion DATE, - date_changement_statut DATE, - motif_statut VARCHAR(500), - approuve_par_id UUID, - - -- Métadonnées BaseEntity - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_mo_utilisateur FOREIGN KEY (utilisateur_id) REFERENCES utilisateurs(id) ON DELETE CASCADE, - CONSTRAINT fk_mo_organisation FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE, - CONSTRAINT fk_mo_unite FOREIGN KEY (unite_id) REFERENCES organisations(id) ON DELETE SET NULL, - CONSTRAINT fk_mo_approuve_par FOREIGN KEY (approuve_par_id) REFERENCES utilisateurs(id) ON DELETE SET NULL, - CONSTRAINT uk_mo_utilisateur_organisation UNIQUE (utilisateur_id, organisation_id), - CONSTRAINT chk_mo_statut CHECK (statut_membre IN ( - 'EN_ATTENTE_VALIDATION','ACTIF','INACTIF', - 'SUSPENDU','DEMISSIONNAIRE','RADIE','HONORAIRE','DECEDE' - )) + destinataire_id UUID NOT NULL REFERENCES utilisateurs(id), + titre VARCHAR(255) NOT NULL, + message TEXT, + type VARCHAR(50), + priorite VARCHAR(20), + lu BOOLEAN DEFAULT FALSE, + date_lecture TIMESTAMP, + url_action VARCHAR(500), + metadata TEXT, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE INDEX idx_mo_utilisateur ON membres_organisations(utilisateur_id); -CREATE INDEX idx_mo_organisation ON membres_organisations(organisation_id); -CREATE INDEX idx_mo_statut ON membres_organisations(statut_membre); -CREATE INDEX idx_mo_unite ON membres_organisations(unite_id); - --- Table agrements_professionnels (registre) — entité AgrementProfessionnel -CREATE TABLE IF NOT EXISTS agrements_professionnels ( +-- Table template_notification (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS template_notification ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - membre_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, - organisation_id UUID NOT NULL REFERENCES organisations(id) ON DELETE CASCADE, - secteur_ordre VARCHAR(150), - numero_licence VARCHAR(100), - categorie_classement VARCHAR(100), - date_delivrance DATE, - date_expiration DATE, - statut VARCHAR(50) NOT NULL DEFAULT 'PROVISOIRE', - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + code VARCHAR(100) NOT NULL UNIQUE, + nom VARCHAR(200) NOT NULL, + sujet VARCHAR(255), + corps TEXT, + type VARCHAR(50), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table document (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS document ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + titre VARCHAR(255) NOT NULL, + description TEXT, + type_document VARCHAR(50), + chemin_fichier VARCHAR(500), + taille_octets BIGINT, + mime_type VARCHAR(100), + proprietaire_id UUID REFERENCES utilisateurs(id), + organisation_id UUID REFERENCES organisations(id), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - actif BOOLEAN NOT NULL DEFAULT TRUE, - CONSTRAINT chk_agrement_statut CHECK (statut IN ('PROVISOIRE','VALIDE','SUSPENDU','RETRETIRE')) + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE INDEX IF NOT EXISTS idx_agrement_membre ON agrements_professionnels(membre_id); -CREATE INDEX IF NOT EXISTS idx_agrement_orga ON agrements_professionnels(organisation_id); --- Mettre à jour les FK des tables existantes qui pointaient sur membres(id) -ALTER TABLE cotisations - DROP CONSTRAINT IF EXISTS fk_cotisation_membre, - ADD CONSTRAINT fk_cotisation_membre FOREIGN KEY (membre_id) REFERENCES utilisateurs(id); - -ALTER TABLE inscriptions_evenement - DROP CONSTRAINT IF EXISTS fk_inscription_membre, - ADD CONSTRAINT fk_inscription_membre FOREIGN KEY (membre_id) REFERENCES utilisateurs(id); - -ALTER TABLE demandes_aide - DROP CONSTRAINT IF EXISTS fk_demande_demandeur, - DROP CONSTRAINT IF EXISTS fk_demande_evaluateur, - ADD CONSTRAINT fk_demande_demandeur FOREIGN KEY (demandeur_id) REFERENCES utilisateurs(id), - ADD CONSTRAINT fk_demande_evaluateur FOREIGN KEY (evaluateur_id) REFERENCES utilisateurs(id) ON DELETE SET NULL; - -COMMENT ON TABLE utilisateurs IS 'Identité globale unique de chaque utilisateur UnionFlow (1 compte = 1 profil)'; -COMMENT ON TABLE membres_organisations IS 'Lien utilisateur ↔ organisation avec statut de membership'; -COMMENT ON COLUMN membres_organisations.unite_id IS 'Agence/bureau d''affectation au sein de la hiérarchie. NULL = siège'; - - --- ========== V2.1__Organisations_Hierarchy.sql ========== --- ============================================================ --- V2.1 — Hiérarchie organisations + corrections --- Auteur: UnionFlow Team | BCEAO/OHADA compliant --- ============================================================ - --- Ajouter la FK propre pour la hiérarchie (remplace le UUID nu) -ALTER TABLE organisations - DROP CONSTRAINT IF EXISTS fk_organisation_parente; - -ALTER TABLE organisations - ADD CONSTRAINT fk_organisation_parente - FOREIGN KEY (organisation_parente_id) REFERENCES organisations(id) ON DELETE SET NULL; - --- Nouveaux champs hiérarchie et modules -ALTER TABLE organisations - ADD COLUMN IF NOT EXISTS est_organisation_racine BOOLEAN NOT NULL DEFAULT TRUE, - ADD COLUMN IF NOT EXISTS chemin_hierarchique VARCHAR(2000), - ADD COLUMN IF NOT EXISTS type_organisation_code VARCHAR(50); - --- Élargir la contrainte de type_organisation pour couvrir tous les métiers -ALTER TABLE organisations DROP CONSTRAINT IF EXISTS chk_organisation_type; -ALTER TABLE organisations - ADD CONSTRAINT chk_organisation_type CHECK (type_organisation IN ( - 'ASSOCIATION','MUTUELLE_EPARGNE_CREDIT','MUTUELLE_SANTE', - 'TONTINE','ONG','COOPERATIVE_AGRICOLE','ASSOCIATION_PROFESSIONNELLE', - 'ASSOCIATION_COMMUNAUTAIRE','ORGANISATION_RELIGIEUSE', - 'FEDERATION','SYNDICAT','LIONS_CLUB','ROTARY_CLUB','AUTRE' - )); - --- Règle : organisation sans parent = racine -UPDATE organisations - SET est_organisation_racine = TRUE - WHERE organisation_parente_id IS NULL; - -UPDATE organisations - SET est_organisation_racine = FALSE - WHERE organisation_parente_id IS NOT NULL; - --- Index pour les requêtes hiérarchiques -CREATE INDEX IF NOT EXISTS idx_org_racine ON organisations(est_organisation_racine); -CREATE INDEX IF NOT EXISTS idx_org_chemin ON organisations(chemin_hierarchique); - -COMMENT ON COLUMN organisations.est_organisation_racine IS 'TRUE si c''est l''organisation mère (souscrit au forfait pour toute la hiérarchie)'; -COMMENT ON COLUMN organisations.chemin_hierarchique IS 'Chemin UUID ex: /uuid-racine/uuid-inter/uuid-feuille — requêtes récursives optimisées'; - - --- ========== V2.2__SaaS_Souscriptions.sql ========== --- ============================================================ --- V2.2 — SaaS : formules_abonnement + souscriptions_organisation --- Auteur: UnionFlow Team | BCEAO/OHADA compliant --- ============================================================ - -CREATE TABLE IF NOT EXISTS formules_abonnement ( +-- Table pieces_jointes +CREATE TABLE IF NOT EXISTS pieces_jointes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom_fichier VARCHAR(255) NOT NULL, + chemin_fichier VARCHAR(500), + taille_octets BIGINT, + mime_type VARCHAR(100), + entite_type VARCHAR(50), + entite_id UUID, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + cree_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); - code VARCHAR(20) UNIQUE NOT NULL, -- STARTER, STANDARD, PREMIUM, CRYSTAL - libelle VARCHAR(100) NOT NULL, - description TEXT, - max_membres INTEGER, -- NULL = illimité (Crystal+) - max_stockage_mo INTEGER NOT NULL DEFAULT 1024, -- 1 Go par défaut - prix_mensuel DECIMAL(10,2) NOT NULL CHECK (prix_mensuel >= 0), - prix_annuel DECIMAL(10,2) NOT NULL CHECK (prix_annuel >= 0), - actif BOOLEAN NOT NULL DEFAULT TRUE, - ordre_affichage INTEGER DEFAULT 0, +-- ============================================================================ +-- 8. TABLES WORKFLOWS & FINANCE AVANCÉE +-- ============================================================================ - -- Métadonnées BaseEntity - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +-- Table transaction_approvals (workflow approbations) +CREATE TABLE IF NOT EXISTS transaction_approvals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + transaction_type VARCHAR(50) NOT NULL, + transaction_id UUID NOT NULL, + current_level INTEGER DEFAULT 1, + required_levels INTEGER DEFAULT 1, + status VARCHAR(30) DEFAULT 'PENDING', + initiated_by UUID REFERENCES utilisateurs(id), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT chk_formule_code CHECK (code IN ('STARTER','STANDARD','PREMIUM','CRYSTAL')) + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); --- Données initiales des forfaits (XOF, 1er Janvier 2026) -INSERT INTO formules_abonnement (id, code, libelle, description, max_membres, max_stockage_mo, prix_mensuel, prix_annuel, actif, ordre_affichage) -VALUES - (gen_random_uuid(), 'STARTER', 'Formule Starter', 'Idéal pour démarrer — jusqu''à 50 membres', 50, 1024, 5000.00, 50000.00, true, 1), - (gen_random_uuid(), 'STANDARD', 'Formule Standard', 'Pour les organisations en croissance', 200, 1024, 7000.00, 70000.00, true, 2), - (gen_random_uuid(), 'PREMIUM', 'Formule Premium', 'Organisations établies', 500, 1024, 9000.00, 90000.00, true, 3), - (gen_random_uuid(), 'CRYSTAL', 'Formule Crystal', 'Fédérations et grandes organisations', NULL,1024, 10000.00, 100000.00, true, 4) -ON CONFLICT (code) DO NOTHING; - --- ============================================================ -CREATE TABLE IF NOT EXISTS souscriptions_organisation ( +-- Table approver_actions +CREATE TABLE IF NOT EXISTS approver_actions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - organisation_id UUID UNIQUE NOT NULL, - formule_id UUID NOT NULL, - type_periode VARCHAR(10) NOT NULL DEFAULT 'MENSUEL', -- MENSUEL | ANNUEL - date_debut DATE NOT NULL, - date_fin DATE NOT NULL, - quota_max INTEGER, -- snapshot de formule.max_membres - quota_utilise INTEGER NOT NULL DEFAULT 0, - statut VARCHAR(30) NOT NULL DEFAULT 'ACTIVE', - reference_paiement_wave VARCHAR(100), - wave_session_id VARCHAR(255), - date_dernier_paiement DATE, - date_prochain_paiement DATE, - - -- Métadonnées BaseEntity - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_souscription_org FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE, - CONSTRAINT fk_souscription_formule FOREIGN KEY (formule_id) REFERENCES formules_abonnement(id), - CONSTRAINT chk_souscription_statut CHECK (statut IN ('ACTIVE','EXPIREE','SUSPENDUE','RESILIEE')), - CONSTRAINT chk_souscription_periode CHECK (type_periode IN ('MENSUEL','ANNUEL')), - CONSTRAINT chk_souscription_quota CHECK (quota_utilise >= 0) + approval_id UUID NOT NULL REFERENCES transaction_approvals(id) ON DELETE CASCADE, + approver_id UUID NOT NULL REFERENCES utilisateurs(id), + level INTEGER NOT NULL, + action VARCHAR(20) NOT NULL, + comment TEXT, + action_date TIMESTAMP DEFAULT NOW(), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE INDEX idx_souscription_org ON souscriptions_organisation(organisation_id); -CREATE INDEX idx_souscription_statut ON souscriptions_organisation(statut); -CREATE INDEX idx_souscription_fin ON souscriptions_organisation(date_fin); - -COMMENT ON TABLE formules_abonnement IS 'Catalogue des forfaits SaaS UnionFlow (Starter→Crystal, 5000–10000 XOF/mois)'; -COMMENT ON TABLE souscriptions_organisation IS 'Abonnement actif d''une organisation racine — quota, durée, référence Wave'; -COMMENT ON COLUMN souscriptions_organisation.quota_utilise IS 'Incrémenté automatiquement à chaque adhésion validée. Bloquant si = quota_max.'; - - --- ========== V2.3__Intentions_Paiement.sql ========== --- ============================================================ --- V2.3 — Hub de paiement Wave : intentions_paiement --- Chaque paiement Wave est initié depuis UnionFlow. --- Auteur: UnionFlow Team | BCEAO/OHADA compliant --- ============================================================ - -CREATE TABLE IF NOT EXISTS intentions_paiement ( +-- Table budgets +CREATE TABLE IF NOT EXISTS budgets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - utilisateur_id UUID NOT NULL, - organisation_id UUID, -- NULL pour abonnements UnionFlow SA - montant_total DECIMAL(14,2) NOT NULL CHECK (montant_total > 0), - code_devise VARCHAR(3) NOT NULL DEFAULT 'XOF', - type_objet VARCHAR(30) NOT NULL, -- COTISATION|ADHESION|EVENEMENT|ABONNEMENT_UNIONFLOW - statut VARCHAR(20) NOT NULL DEFAULT 'INITIEE', - - -- Wave API - wave_checkout_session_id VARCHAR(255) UNIQUE, - wave_launch_url VARCHAR(1000), - wave_transaction_id VARCHAR(100), - - -- Traçabilité des objets payés (JSON: [{type,id,montant},...]) - objets_cibles TEXT, - - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_expiration TIMESTAMP, -- TTL 30 min - date_completion TIMESTAMP, - - -- Métadonnées BaseEntity - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_intention_utilisateur FOREIGN KEY (utilisateur_id) REFERENCES utilisateurs(id), - CONSTRAINT fk_intention_organisation FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE SET NULL, - CONSTRAINT chk_intention_type CHECK (type_objet IN ('COTISATION','ADHESION','EVENEMENT','ABONNEMENT_UNIONFLOW')), - CONSTRAINT chk_intention_statut CHECK (statut IN ('INITIEE','EN_COURS','COMPLETEE','EXPIREE','ECHOUEE')), - CONSTRAINT chk_intention_devise CHECK (code_devise ~ '^[A-Z]{3}$') + nom VARCHAR(200) NOT NULL, + code VARCHAR(50) UNIQUE, + organisation_id UUID REFERENCES organisations(id), + exercice_annee INTEGER NOT NULL, + montant_total DECIMAL(14,2) NOT NULL, + montant_utilise DECIMAL(14,2) DEFAULT 0, + statut VARCHAR(30) DEFAULT 'ACTIF', + date_debut DATE, + date_fin DATE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE INDEX idx_intention_utilisateur ON intentions_paiement(utilisateur_id); -CREATE INDEX idx_intention_statut ON intentions_paiement(statut); -CREATE INDEX idx_intention_wave_session ON intentions_paiement(wave_checkout_session_id); -CREATE INDEX idx_intention_expiration ON intentions_paiement(date_expiration); - --- Supprimer les champs paiement redondants de cotisations (centralisés dans intentions_paiement) -ALTER TABLE cotisations - DROP COLUMN IF EXISTS methode_paiement, - DROP COLUMN IF EXISTS reference_paiement; - --- Ajouter le lien cotisation → intention de paiement -ALTER TABLE cotisations - ADD COLUMN IF NOT EXISTS intention_paiement_id UUID, - ADD CONSTRAINT fk_cotisation_intention - FOREIGN KEY (intention_paiement_id) REFERENCES intentions_paiement(id) ON DELETE SET NULL; - -COMMENT ON TABLE intentions_paiement IS 'Hub centralisé Wave : chaque paiement est initié depuis UnionFlow avant appel API Wave'; -COMMENT ON COLUMN intentions_paiement.objets_cibles IS 'JSON: liste des objets couverts par ce paiement — ex: 3 cotisations mensuelles'; -COMMENT ON COLUMN intentions_paiement.wave_checkout_session_id IS 'ID de session Wave — clé de réconciliation sur réception webhook'; - - --- ========== V2.4__Cotisations_Organisation.sql ========== --- ============================================================ --- V2.4 — Cotisations : ajout organisation_id + parametres --- Une cotisation est toujours liée à un membre ET à une organisation --- Auteur: UnionFlow Team | BCEAO/OHADA compliant --- ============================================================ - --- Ajouter organisation_id sur cotisations -ALTER TABLE cotisations - ADD COLUMN IF NOT EXISTS organisation_id UUID; - -ALTER TABLE cotisations - ADD CONSTRAINT fk_cotisation_organisation - FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE; - -CREATE INDEX IF NOT EXISTS idx_cotisation_organisation ON cotisations(organisation_id); - --- Mettre à jour les types de cotisation -ALTER TABLE cotisations DROP CONSTRAINT IF EXISTS chk_cotisation_type; -ALTER TABLE cotisations - ADD CONSTRAINT chk_cotisation_type CHECK (type_cotisation IN ( - 'ANNUELLE','MENSUELLE','EVENEMENTIELLE','SOLIDARITE','EXCEPTIONNELLE','AUTRE' - )); - --- ============================================================ --- Paramètres de cotisation par organisation (montants fixés par l'org) --- ============================================================ -CREATE TABLE IF NOT EXISTS parametres_cotisation_organisation ( +-- Table budget_lines +CREATE TABLE IF NOT EXISTS budget_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - organisation_id UUID UNIQUE NOT NULL, - montant_cotisation_mensuelle DECIMAL(12,2) DEFAULT 0 CHECK (montant_cotisation_mensuelle >= 0), - montant_cotisation_annuelle DECIMAL(12,2) DEFAULT 0 CHECK (montant_cotisation_annuelle >= 0), - devise VARCHAR(3) NOT NULL DEFAULT 'XOF', - date_debut_calcul_ajour DATE, -- configurable: depuis quand calculer les impayés - delai_retard_avant_inactif_jours INTEGER NOT NULL DEFAULT 30, - cotisation_obligatoire BOOLEAN NOT NULL DEFAULT TRUE, - - -- Métadonnées BaseEntity - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_param_cotisation_org FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE + budget_id UUID NOT NULL REFERENCES budgets(id) ON DELETE CASCADE, + libelle VARCHAR(255) NOT NULL, + categorie VARCHAR(50), + montant_prevu DECIMAL(14,2) NOT NULL, + montant_engage DECIMAL(14,2) DEFAULT 0, + montant_realise DECIMAL(14,2) DEFAULT 0, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -COMMENT ON TABLE parametres_cotisation_organisation IS 'Paramètres de cotisation configurés par le manager de chaque organisation'; -COMMENT ON COLUMN parametres_cotisation_organisation.date_debut_calcul_ajour IS 'Date de référence pour le calcul membre «à jour». Configurable par le manager.'; -COMMENT ON COLUMN parametres_cotisation_organisation.delai_retard_avant_inactif_jours IS 'Jours de retard après lesquels un membre passe INACTIF automatiquement'; - - --- ========== V2.5__Workflow_Solidarite.sql ========== --- ============================================================ --- V2.5 — Workflow solidarité configurable (max 3 étapes) --- + demandes_adhesion (remplace adhesions) --- Auteur: UnionFlow Team | BCEAO/OHADA compliant --- ============================================================ - --- ============================================================ --- Workflow de validation configurable par organisation --- ============================================================ +-- Table workflow_validation_config CREATE TABLE IF NOT EXISTS workflow_validation_config ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - organisation_id UUID NOT NULL, - type_workflow VARCHAR(30) NOT NULL DEFAULT 'DEMANDE_AIDE', - etape_numero INTEGER NOT NULL CHECK (etape_numero BETWEEN 1 AND 3), - role_requis_id UUID, -- rôle nécessaire pour valider cette étape - libelle_etape VARCHAR(200) NOT NULL, - delai_max_heures INTEGER DEFAULT 72, - actif BOOLEAN NOT NULL DEFAULT TRUE, - - -- Métadonnées BaseEntity - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + type_transaction VARCHAR(50) NOT NULL, + niveau INTEGER NOT NULL, + role_validateur VARCHAR(100) NOT NULL, + montant_min DECIMAL(14,2), + montant_max DECIMAL(14,2), + organisation_id UUID REFERENCES organisations(id), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_wf_organisation FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE, - CONSTRAINT fk_wf_role FOREIGN KEY (role_requis_id) REFERENCES roles(id) ON DELETE SET NULL, - CONSTRAINT uk_wf_org_type_etape UNIQUE (organisation_id, type_workflow, etape_numero), - CONSTRAINT chk_wf_type CHECK (type_workflow IN ('DEMANDE_AIDE','ADHESION','AUTRE')) + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE INDEX idx_wf_organisation ON workflow_validation_config(organisation_id); -CREATE INDEX idx_wf_type ON workflow_validation_config(type_workflow); +-- ============================================================================ +-- 9. TABLES MONITORING & LOGS +-- ============================================================================ --- ============================================================ --- Historique des validations d'une demande d'aide --- ============================================================ -CREATE TABLE IF NOT EXISTS validation_etapes_demande ( +-- Table system_logs +CREATE TABLE IF NOT EXISTS system_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + niveau VARCHAR(20) NOT NULL, + source VARCHAR(100), + message TEXT, + stacktrace TEXT, + utilisateur_id UUID, + ip_address VARCHAR(50), + user_agent VARCHAR(500), + metadata TEXT, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + cree_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); - demande_aide_id UUID NOT NULL, - etape_numero INTEGER NOT NULL CHECK (etape_numero BETWEEN 1 AND 3), - valideur_id UUID, - statut VARCHAR(20) NOT NULL DEFAULT 'EN_ATTENTE', - date_validation TIMESTAMP, - commentaire VARCHAR(1000), - delegue_par_id UUID, -- si désactivation du véto par supérieur - trace_delegation TEXT, -- motif + traçabilité BCEAO/OHADA +CREATE INDEX idx_system_logs_niveau ON system_logs(niveau); +CREATE INDEX idx_system_logs_date ON system_logs(date_creation DESC); - -- Métadonnées BaseEntity - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +-- Table system_alerts +CREATE TABLE IF NOT EXISTS system_alerts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type_alerte VARCHAR(50) NOT NULL, + severite VARCHAR(20) NOT NULL, + titre VARCHAR(255) NOT NULL, + message TEXT, + statut VARCHAR(30) DEFAULT 'NOUVELLE', + date_alerte TIMESTAMP DEFAULT NOW(), + date_resolution TIMESTAMP, + resolu_par UUID REFERENCES utilisateurs(id), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_ved_demande FOREIGN KEY (demande_aide_id) REFERENCES demandes_aide(id) ON DELETE CASCADE, - CONSTRAINT fk_ved_valideur FOREIGN KEY (valideur_id) REFERENCES utilisateurs(id) ON DELETE SET NULL, - CONSTRAINT fk_ved_delegue_par FOREIGN KEY (delegue_par_id) REFERENCES utilisateurs(id) ON DELETE SET NULL, - CONSTRAINT chk_ved_statut CHECK (statut IN ('EN_ATTENTE','APPROUVEE','REJETEE','DELEGUEE','EXPIREE')) + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE INDEX idx_ved_demande ON validation_etapes_demande(demande_aide_id); -CREATE INDEX idx_ved_valideur ON validation_etapes_demande(valideur_id); -CREATE INDEX idx_ved_statut ON validation_etapes_demande(statut); - --- ============================================================ --- demandes_adhesion (remplace adhesions avec modèle enrichi) --- ============================================================ -DROP TABLE IF EXISTS adhesions CASCADE; - -CREATE TABLE IF NOT EXISTS demandes_adhesion ( +-- Table alert_configuration +CREATE TABLE IF NOT EXISTS alert_configuration ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - numero_reference VARCHAR(50) UNIQUE NOT NULL, - utilisateur_id UUID NOT NULL, - organisation_id UUID NOT NULL, - statut VARCHAR(20) NOT NULL DEFAULT 'EN_ATTENTE', - frais_adhesion DECIMAL(12,2) NOT NULL DEFAULT 0 CHECK (frais_adhesion >= 0), - montant_paye DECIMAL(12,2) NOT NULL DEFAULT 0, - code_devise VARCHAR(3) NOT NULL DEFAULT 'XOF', - intention_paiement_id UUID, - date_demande TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_traitement TIMESTAMP, - traite_par_id UUID, - motif_rejet VARCHAR(1000), - observations VARCHAR(1000), - - -- Métadonnées BaseEntity - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_da_utilisateur FOREIGN KEY (utilisateur_id) REFERENCES utilisateurs(id) ON DELETE CASCADE, - CONSTRAINT fk_da_organisation FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE, - CONSTRAINT fk_da_intention FOREIGN KEY (intention_paiement_id) REFERENCES intentions_paiement(id) ON DELETE SET NULL, - CONSTRAINT fk_da_traite_par FOREIGN KEY (traite_par_id) REFERENCES utilisateurs(id) ON DELETE SET NULL, - CONSTRAINT chk_da_statut CHECK (statut IN ('EN_ATTENTE','APPROUVEE','REJETEE','ANNULEE')) -); - -CREATE INDEX idx_da_utilisateur ON demandes_adhesion(utilisateur_id); -CREATE INDEX idx_da_organisation ON demandes_adhesion(organisation_id); -CREATE INDEX idx_da_statut ON demandes_adhesion(statut); -CREATE INDEX idx_da_date ON demandes_adhesion(date_demande); - -COMMENT ON TABLE workflow_validation_config IS 'Configuration du workflow de validation par organisation (max 3 étapes)'; -COMMENT ON TABLE validation_etapes_demande IS 'Historique des validations — tracé BCEAO/OHADA — délégation de véto incluse'; -COMMENT ON TABLE demandes_adhesion IS 'Demande d''adhésion d''un utilisateur à une organisation avec paiement Wave intégré'; - - --- ========== V2.6__Modules_Organisation.sql ========== --- ============================================================ --- V2.6 — Système de modules activables par type d'organisation --- Auteur: UnionFlow Team | BCEAO/OHADA compliant --- ============================================================ - -CREATE TABLE IF NOT EXISTS modules_disponibles ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - code VARCHAR(50) UNIQUE NOT NULL, - libelle VARCHAR(150) NOT NULL, - description TEXT, - types_org_compatibles TEXT, -- JSON array: ["MUTUELLE_SANTE","ONG",...] - actif BOOLEAN NOT NULL DEFAULT TRUE, - ordre_affichage INTEGER DEFAULT 0, - - -- Métadonnées BaseEntity - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0 -); - --- Catalogue initial des modules métier -INSERT INTO modules_disponibles (id, code, libelle, description, types_org_compatibles, actif, ordre_affichage) -VALUES - (gen_random_uuid(), 'COTISATIONS', 'Gestion des cotisations', 'Suivi cotisations, relances, statistiques', '["ALL"]', true, 1), - (gen_random_uuid(), 'EVENEMENTS', 'Gestion des événements', 'Création, inscriptions, présences, paiements', '["ALL"]', true, 2), - (gen_random_uuid(), 'SOLIDARITE', 'Fonds de solidarité', 'Demandes d''aide avec workflow de validation', '["ALL"]', true, 3), - (gen_random_uuid(), 'COMPTABILITE', 'Comptabilité simplifiée', 'Journal, écritures, comptes — conforme OHADA', '["ALL"]', true, 4), - (gen_random_uuid(), 'DOCUMENTS', 'Gestion documentaire', 'Upload, versioning, intégrité hash — 1Go max', '["ALL"]', true, 5), - (gen_random_uuid(), 'NOTIFICATIONS', 'Notifications multi-canal', 'Email, WhatsApp, push mobile', '["ALL"]', true, 6), - (gen_random_uuid(), 'CREDIT_EPARGNE', 'Épargne & crédit MEC', 'Prêts, échéanciers, impayés, multi-caisses', '["MUTUELLE_EPARGNE_CREDIT"]', true, 10), - (gen_random_uuid(), 'AYANTS_DROIT', 'Gestion des ayants droit', 'Couverture santé, plafonds, conventions centres de santé', '["MUTUELLE_SANTE"]', true, 11), - (gen_random_uuid(), 'TONTINE', 'Tontine / épargne rotative', 'Cycles rotatifs, tirage, enchères, pénalités', '["TONTINE"]', true, 12), - (gen_random_uuid(), 'ONG_PROJETS', 'Projets humanitaires', 'Logframe, budget bailleurs, indicateurs d''impact, rapports', '["ONG"]', true, 13), - (gen_random_uuid(), 'COOP_AGRICOLE', 'Coopérative agricole', 'Parcelles, rendements, intrants, vente groupée, ristournes', '["COOPERATIVE_AGRICOLE"]', true, 14), - (gen_random_uuid(), 'VOTE_INTERNE', 'Vote interne électronique', 'Assemblées générales, votes, quorums', '["FEDERATION","ASSOCIATION","SYNDICAT"]', true, 15), - (gen_random_uuid(), 'COLLECTE_FONDS', 'Collecte de fonds', 'Campagnes de don, suivi, rapports', '["ONG","ORGANISATION_RELIGIEUSE","ASSOCIATION"]', true, 16), - (gen_random_uuid(), 'REGISTRE_PROFESSIONNEL','Registre officiel membres', 'Agrément, diplômes, sanctions disciplinaires, annuaire certifié', '["ASSOCIATION_PROFESSIONNELLE"]', true, 17), - (gen_random_uuid(), 'CULTES_RELIGIEUX', 'Gestion cultes & dîmes', 'Dîmes, promesses de don, planification cultes, cellules, offrandes anon.','["ORGANISATION_RELIGIEUSE"]', true, 18), - (gen_random_uuid(), 'GOUVERNANCE_MULTI', 'Gouvernance multi-niveaux', 'Cotisation par section, reporting consolidé, redistribution subventions', '["FEDERATION"]', true, 19) -ON CONFLICT (code) DO NOTHING; - --- ============================================================ --- Modules activés pour chaque organisation --- ============================================================ -CREATE TABLE IF NOT EXISTS modules_organisation_actifs ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - organisation_id UUID NOT NULL, - module_code VARCHAR(50) NOT NULL, - actif BOOLEAN NOT NULL DEFAULT TRUE, - parametres TEXT, -- JSON de configuration spécifique - date_activation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - - -- Métadonnées BaseEntity - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + type_alerte VARCHAR(50) NOT NULL UNIQUE, + seuil_critique DECIMAL(14,2), + seuil_warning DECIMAL(14,2), + actif BOOLEAN DEFAULT TRUE, + notification_email BOOLEAN DEFAULT TRUE, + notification_sms BOOLEAN DEFAULT FALSE, + destinataires TEXT, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_moa_organisation FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE, - CONSTRAINT uk_moa_org_module UNIQUE (organisation_id, module_code) + version BIGINT DEFAULT 0 ); -CREATE INDEX idx_moa_organisation ON modules_organisation_actifs(organisation_id); -CREATE INDEX idx_moa_module ON modules_organisation_actifs(module_code); - -COMMENT ON TABLE modules_disponibles IS 'Catalogue des modules métier UnionFlow activables selon le type d''organisation'; -COMMENT ON TABLE modules_organisation_actifs IS 'Modules activés pour une organisation donnée avec paramètres spécifiques'; - - --- ========== V2.7__Ayants_Droit.sql ========== --- ============================================================ --- V2.7 — Ayants droit (mutuelles de santé) --- Auteur: UnionFlow Team | BCEAO/OHADA compliant --- ============================================================ - -CREATE TABLE IF NOT EXISTS ayants_droit ( +-- Table audit_logs +CREATE TABLE IF NOT EXISTS audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - - membre_organisation_id UUID NOT NULL, -- membre dans le contexte org mutuelle - prenom VARCHAR(100) NOT NULL, - nom VARCHAR(100) NOT NULL, - date_naissance DATE, - lien_parente VARCHAR(20) NOT NULL, -- CONJOINT|ENFANT|PARENT|AUTRE - numero_beneficiaire VARCHAR(50), -- numéro pour les conventions santé - date_debut_couverture DATE, - date_fin_couverture DATE, - - -- Métadonnées BaseEntity - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - - CONSTRAINT fk_ad_membre_org FOREIGN KEY (membre_organisation_id) REFERENCES membres_organisations(id) ON DELETE CASCADE, - CONSTRAINT chk_ad_lien_parente CHECK (lien_parente IN ('CONJOINT','ENFANT','PARENT','AUTRE')) + entite_type VARCHAR(100) NOT NULL, + entite_id UUID, + action VARCHAR(50) NOT NULL, + utilisateur_id UUID, + ancien_etat TEXT, + nouvel_etat TEXT, + ip_address VARCHAR(50), + date_action TIMESTAMP DEFAULT NOW(), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE INDEX idx_ad_membre_org ON ayants_droit(membre_organisation_id); -CREATE INDEX idx_ad_couverture ON ayants_droit(date_debut_couverture, date_fin_couverture); +CREATE INDEX idx_audit_entite ON audit_logs(entite_type, entite_id); +CREATE INDEX idx_audit_date ON audit_logs(date_action DESC); -COMMENT ON TABLE ayants_droit IS 'Bénéficiaires d''un membre dans une mutuelle de santé (conjoint, enfants, parents)'; -COMMENT ON COLUMN ayants_droit.numero_beneficiaire IS 'Numéro unique attribué pour les conventions avec les centres de santé partenaires'; +-- ============================================================================ +-- 10. TABLES MÉTIER SPÉCIALISÉES +-- ============================================================================ - --- ========== V2.8__Roles_Par_Organisation.sql ========== --- ============================================================ --- V2.8 — Rôles par organisation : membres_roles enrichi --- Un membre peut avoir des rôles différents selon l'organisation --- Auteur: UnionFlow Team | BCEAO/OHADA compliant --- ============================================================ - --- membres_roles doit référencer membres_organisations (pas uniquement membres) --- On ajoute organisation_id et membre_organisation_id pour permettre les rôles multi-org - -ALTER TABLE membres_roles - ADD COLUMN IF NOT EXISTS membre_organisation_id UUID, - ADD COLUMN IF NOT EXISTS organisation_id UUID; - --- Mettre à jour la FK et la contrainte UNIQUE -ALTER TABLE membres_roles - DROP CONSTRAINT IF EXISTS uk_membre_role; - -ALTER TABLE membres_roles - ADD CONSTRAINT fk_mr_membre_org FOREIGN KEY (membre_organisation_id) REFERENCES membres_organisations(id) ON DELETE CASCADE, - ADD CONSTRAINT fk_mr_organisation FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE; - --- Nouvelle contrainte: un utilisateur ne peut avoir le même rôle qu'une fois par organisation -ALTER TABLE membres_roles - ADD CONSTRAINT uk_mr_membre_org_role - UNIQUE (membre_organisation_id, role_id); - -CREATE INDEX IF NOT EXISTS idx_mr_membre_org ON membres_roles(membre_organisation_id); -CREATE INDEX IF NOT EXISTS idx_mr_organisation ON membres_roles(organisation_id); - -COMMENT ON COLUMN membres_roles.membre_organisation_id IS 'Lien vers le membership de l''utilisateur dans l''organisation — détermine le contexte du rôle'; -COMMENT ON COLUMN membres_roles.organisation_id IS 'Organisation dans laquelle ce rôle est actif — dénormalisé pour les requêtes de performance'; - - --- ========== V2.9__Audit_Enhancements.sql ========== --- ============================================================ --- V2.9 — Améliorations audit_logs : portée + organisation --- Double niveau : ORGANISATION (manager) + PLATEFORME (super admin) --- Conservation 10 ans — BCEAO/OHADA/Fiscalité ivoirienne --- Auteur: UnionFlow Team --- ============================================================ - -ALTER TABLE audit_logs - ADD COLUMN IF NOT EXISTS organisation_id UUID, - ADD COLUMN IF NOT EXISTS portee VARCHAR(15) NOT NULL DEFAULT 'PLATEFORME'; - -ALTER TABLE audit_logs - ADD CONSTRAINT fk_audit_organisation FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE SET NULL, - ADD CONSTRAINT chk_audit_portee CHECK (portee IN ('ORGANISATION','PLATEFORME')); - -CREATE INDEX IF NOT EXISTS idx_audit_organisation ON audit_logs(organisation_id); -CREATE INDEX IF NOT EXISTS idx_audit_portee ON audit_logs(portee); - --- Index composite pour les consultations fréquentes -CREATE INDEX IF NOT EXISTS idx_audit_org_portee_date ON audit_logs(organisation_id, portee, date_heure DESC); - -COMMENT ON COLUMN audit_logs.organisation_id IS 'Organisation concernée — NULL pour événements plateforme'; -COMMENT ON COLUMN audit_logs.portee IS 'ORGANISATION: visible par le manager | PLATEFORME: visible uniquement par Super Admin UnionFlow'; - - --- ========== V2.10__Devises_Africaines_Uniquement.sql ========== --- ============================================================ --- V2.10 — Devises : liste strictement africaine --- Remplace EUR, USD, GBP, CHF par des codes africains (XOF par défaut) --- ============================================================ - --- Migrer les organisations avec une devise non africaine vers XOF -UPDATE organisations -SET devise = 'XOF' -WHERE devise IS NOT NULL - AND devise NOT IN ('XOF', 'XAF', 'MAD', 'DZD', 'TND', 'NGN', 'GHS', 'KES', 'ZAR'); - --- Remplacer la contrainte par une liste africaine uniquement -ALTER TABLE organisations DROP CONSTRAINT IF EXISTS chk_organisation_devise; - -ALTER TABLE organisations -ADD CONSTRAINT chk_organisation_devise CHECK ( - devise IN ('XOF', 'XAF', 'MAD', 'DZD', 'TND', 'NGN', 'GHS', 'KES', 'ZAR') -); - -COMMENT ON COLUMN organisations.devise IS 'Code ISO 4217 — devises africaines uniquement (XOF, XAF, MAD, DZD, TND, NGN, GHS, KES, ZAR)'; - - --- ========== V3.0__Optimisation_Structure_Donnees.sql ========== --- ===================================================== --- V3.0 — Optimisation de la structure de données --- ===================================================== --- Cat.1 : Table types_reference --- Cat.2 : Table paiements_objets + suppression --- colonnes adresse de organisations --- Cat.4 : Refonte pieces_jointes (polymorphique) --- Cat.5 : Colonnes Membre manquantes --- ===================================================== - --- ───────────────────────────────────────────────────── --- Cat.1 — types_reference --- ───────────────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS types_reference ( - id UUID PRIMARY KEY, - domaine VARCHAR(100) NOT NULL, - code VARCHAR(100) NOT NULL, - libelle VARCHAR(255) NOT NULL, - description VARCHAR(1000), - ordre INT NOT NULL DEFAULT 0, - valeur_systeme BOOLEAN NOT NULL DEFAULT FALSE, - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - CONSTRAINT uk_type_ref_domaine_code - UNIQUE (domaine, code) -); - -CREATE INDEX IF NOT EXISTS idx_tr_domaine - ON types_reference (domaine); -CREATE INDEX IF NOT EXISTS idx_tr_actif - ON types_reference (actif); - --- ───────────────────────────────────────────────────────────────────────────── --- Bloc d'idempotence : corrige l'écart entre la table créée par Hibernate --- (sans DEFAULT SQL) et le schéma attendu par cette migration. --- Hibernate gère les defaults en Java ; ici on les pose au niveau PostgreSQL. --- ───────────────────────────────────────────────────────────────────────────── -ALTER TABLE types_reference - ADD COLUMN IF NOT EXISTS valeur_systeme BOOLEAN NOT NULL DEFAULT FALSE; - -ALTER TABLE types_reference - ADD COLUMN IF NOT EXISTS ordre INT NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS actif BOOLEAN NOT NULL DEFAULT TRUE, - ADD COLUMN IF NOT EXISTS version BIGINT NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - ADD COLUMN IF NOT EXISTS est_defaut BOOLEAN NOT NULL DEFAULT FALSE, - ADD COLUMN IF NOT EXISTS est_systeme BOOLEAN NOT NULL DEFAULT FALSE, - ADD COLUMN IF NOT EXISTS ordre_affichage INT NOT NULL DEFAULT 0; - --- Garantit que la contrainte UNIQUE existe (nécessaire pour ON CONFLICT ci-dessous) -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_constraint - WHERE conname = 'uk_type_ref_domaine_code' - AND conrelid = 'types_reference'::regclass - ) THEN - ALTER TABLE types_reference - ADD CONSTRAINT uk_type_ref_domaine_code UNIQUE (domaine, code); - END IF; -END $$; - --- Données initiales : domaines référencés par les entités --- Toutes les colonnes NOT NULL sont fournies (table peut exister sans DEFAULT si créée par Hibernate) -INSERT INTO types_reference (id, domaine, code, libelle, valeur_systeme, cree_par, actif, ordre, version, date_creation, est_defaut, est_systeme, ordre_affichage) -VALUES - -- OBJET_PAIEMENT (Cat.2 — PaiementObjet) - (gen_random_uuid(), 'OBJET_PAIEMENT', 'COTISATION', 'Cotisation', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'OBJET_PAIEMENT', 'ADHESION', 'Adhésion', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'OBJET_PAIEMENT', 'EVENEMENT', 'Événement', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'OBJET_PAIEMENT', 'AIDE', 'Aide', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - -- ENTITE_RATTACHEE (Cat.4 — PieceJointe) - (gen_random_uuid(), 'ENTITE_RATTACHEE', 'MEMBRE', 'Membre', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'ENTITE_RATTACHEE', 'ORGANISATION', 'Organisation', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'ENTITE_RATTACHEE', 'COTISATION', 'Cotisation', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'ENTITE_RATTACHEE', 'ADHESION', 'Adhésion', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'ENTITE_RATTACHEE', 'AIDE', 'Aide', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'ENTITE_RATTACHEE', 'TRANSACTION_WAVE', 'Transaction Wave', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - -- STATUT_MATRIMONIAL (Cat.5 — Membre) - (gen_random_uuid(), 'STATUT_MATRIMONIAL', 'CELIBATAIRE', 'Célibataire', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'STATUT_MATRIMONIAL', 'MARIE', 'Marié(e)', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'STATUT_MATRIMONIAL', 'DIVORCE', 'Divorcé(e)', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'STATUT_MATRIMONIAL', 'VEUF', 'Veuf/Veuve', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - -- TYPE_IDENTITE (Cat.5 — Membre) - (gen_random_uuid(), 'TYPE_IDENTITE', 'CNI', 'Carte Nationale d''Identité', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'TYPE_IDENTITE', 'PASSEPORT', 'Passeport', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'TYPE_IDENTITE', 'PERMIS', 'Permis de conduire', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), - (gen_random_uuid(), 'TYPE_IDENTITE', 'CARTE_SEJOUR','Carte de séjour', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0) -ON CONFLICT (domaine, code) DO NOTHING; - --- ───────────────────────────────────────────────────── --- Cat.2 — paiements_objets (remplace 4 tables) --- ───────────────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS paiements_objets ( - id UUID PRIMARY KEY, - paiement_id UUID NOT NULL - REFERENCES paiements(id), - type_objet_cible VARCHAR(50) NOT NULL, - objet_cible_id UUID NOT NULL, - montant_applique NUMERIC(14,2) NOT NULL, - date_application TIMESTAMP, - commentaire VARCHAR(500), - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT NOW(), - date_modification TIMESTAMP, - cree_par VARCHAR(255), - modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - CONSTRAINT uk_paiement_objet - UNIQUE (paiement_id, type_objet_cible, objet_cible_id) -); - -CREATE INDEX IF NOT EXISTS idx_po_paiement - ON paiements_objets (paiement_id); -CREATE INDEX IF NOT EXISTS idx_po_objet - ON paiements_objets (type_objet_cible, objet_cible_id); -CREATE INDEX IF NOT EXISTS idx_po_type - ON paiements_objets (type_objet_cible); - --- ───────────────────────────────────────────────────── --- Cat.2 — Suppression colonnes adresse de organisations --- ───────────────────────────────────────────────────── -ALTER TABLE organisations - DROP COLUMN IF EXISTS adresse, - DROP COLUMN IF EXISTS ville, - DROP COLUMN IF EXISTS code_postal, - DROP COLUMN IF EXISTS region, - DROP COLUMN IF EXISTS pays; - --- ───────────────────────────────────────────────────── --- Cat.4 — pieces_jointes → polymorphique --- ───────────────────────────────────────────────────── --- Ajout colonnes polymorphiques -ALTER TABLE pieces_jointes - ADD COLUMN IF NOT EXISTS type_entite_rattachee VARCHAR(50), - ADD COLUMN IF NOT EXISTS entite_rattachee_id UUID; - --- Migration des données existantes (colonnes FK explicites ou entite_type/entite_id selon le schéma) -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'pieces_jointes' AND column_name = 'membre_id') THEN - UPDATE pieces_jointes SET type_entite_rattachee = 'MEMBRE', entite_rattachee_id = membre_id WHERE membre_id IS NOT NULL AND (type_entite_rattachee IS NULL OR type_entite_rattachee = ''); - END IF; - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'pieces_jointes' AND column_name = 'organisation_id') THEN - UPDATE pieces_jointes SET type_entite_rattachee = 'ORGANISATION', entite_rattachee_id = organisation_id WHERE organisation_id IS NOT NULL AND (type_entite_rattachee IS NULL OR type_entite_rattachee = ''); - END IF; - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'pieces_jointes' AND column_name = 'cotisation_id') THEN - UPDATE pieces_jointes SET type_entite_rattachee = 'COTISATION', entite_rattachee_id = cotisation_id WHERE cotisation_id IS NOT NULL AND (type_entite_rattachee IS NULL OR type_entite_rattachee = ''); - END IF; - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'pieces_jointes' AND column_name = 'adhesion_id') THEN - UPDATE pieces_jointes SET type_entite_rattachee = 'ADHESION', entite_rattachee_id = adhesion_id WHERE adhesion_id IS NOT NULL AND (type_entite_rattachee IS NULL OR type_entite_rattachee = ''); - END IF; - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'pieces_jointes' AND column_name = 'demande_aide_id') THEN - UPDATE pieces_jointes SET type_entite_rattachee = 'AIDE', entite_rattachee_id = demande_aide_id WHERE demande_aide_id IS NOT NULL AND (type_entite_rattachee IS NULL OR type_entite_rattachee = ''); - END IF; - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'pieces_jointes' AND column_name = 'transaction_wave_id') THEN - UPDATE pieces_jointes SET type_entite_rattachee = 'TRANSACTION_WAVE', entite_rattachee_id = transaction_wave_id WHERE transaction_wave_id IS NOT NULL AND (type_entite_rattachee IS NULL OR type_entite_rattachee = ''); - END IF; - -- Schéma V1.7 : entite_type / entite_id - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'pieces_jointes' AND column_name = 'entite_type') THEN - UPDATE pieces_jointes SET type_entite_rattachee = COALESCE(NULLIF(TRIM(entite_type), ''), 'MEMBRE'), entite_rattachee_id = entite_id WHERE entite_id IS NOT NULL AND (type_entite_rattachee IS NULL OR type_entite_rattachee = ''); - END IF; - -- Valeurs par défaut pour lignes restantes (évite échec NOT NULL) - UPDATE pieces_jointes SET type_entite_rattachee = COALESCE(NULLIF(TRIM(type_entite_rattachee), ''), 'MEMBRE'), entite_rattachee_id = COALESCE(entite_rattachee_id, (SELECT id FROM utilisateurs LIMIT 1)) WHERE type_entite_rattachee IS NULL OR type_entite_rattachee = '' OR entite_rattachee_id IS NULL; -END $$; - --- Contrainte NOT NULL après migration (seulement si plus aucune ligne NULL) -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM pieces_jointes WHERE type_entite_rattachee IS NULL OR type_entite_rattachee = '' OR entite_rattachee_id IS NULL) THEN - EXECUTE 'ALTER TABLE pieces_jointes ALTER COLUMN type_entite_rattachee SET NOT NULL'; - EXECUTE 'ALTER TABLE pieces_jointes ALTER COLUMN entite_rattachee_id SET NOT NULL'; - END IF; -END $$; - --- Suppression anciennes FK ou colonnes polymorphiques V1.7 (entite_type, entite_id) -ALTER TABLE pieces_jointes - DROP COLUMN IF EXISTS membre_id, - DROP COLUMN IF EXISTS organisation_id, - DROP COLUMN IF EXISTS cotisation_id, - DROP COLUMN IF EXISTS adhesion_id, - DROP COLUMN IF EXISTS demande_aide_id, - DROP COLUMN IF EXISTS transaction_wave_id, - DROP COLUMN IF EXISTS entite_type, - DROP COLUMN IF EXISTS entite_id; - --- Suppression anciens index -DROP INDEX IF EXISTS idx_piece_jointe_membre; -DROP INDEX IF EXISTS idx_piece_jointe_organisation; -DROP INDEX IF EXISTS idx_piece_jointe_cotisation; -DROP INDEX IF EXISTS idx_piece_jointe_adhesion; -DROP INDEX IF EXISTS idx_piece_jointe_demande_aide; -DROP INDEX IF EXISTS idx_piece_jointe_transaction_wave; - --- Nouveaux index polymorphiques -CREATE INDEX IF NOT EXISTS idx_pj_entite - ON pieces_jointes (type_entite_rattachee, entite_rattachee_id); -CREATE INDEX IF NOT EXISTS idx_pj_type_entite - ON pieces_jointes (type_entite_rattachee); - --- ───────────────────────────────────────────────────── --- Cat.5 — Colonnes Membre manquantes (table utilisateurs depuis V2.0) --- ───────────────────────────────────────────────────── -ALTER TABLE utilisateurs - ADD COLUMN IF NOT EXISTS statut_matrimonial VARCHAR(50), - ADD COLUMN IF NOT EXISTS nationalite VARCHAR(100), - ADD COLUMN IF NOT EXISTS type_identite VARCHAR(50), - ADD COLUMN IF NOT EXISTS numero_identite VARCHAR(100); - --- ───────────────────────────────────────────────────── --- Cat.8 — Valeurs par défaut dans configurations --- ───────────────────────────────────────────────────── -INSERT INTO configurations (id, cle, valeur, type, categorie, description, modifiable, visible, actif, date_creation, cree_par, version) -VALUES - (gen_random_uuid(), 'defaut.devise', 'XOF', 'STRING', 'SYSTEME', 'Devise par défaut', TRUE, TRUE, TRUE, NOW(), 'system', 0), - (gen_random_uuid(), 'defaut.statut.organisation', 'ACTIVE', 'STRING', 'SYSTEME', 'Statut initial organisation', TRUE, TRUE, TRUE, NOW(), 'system', 0), - (gen_random_uuid(), 'defaut.type.organisation', 'ASSOCIATION', 'STRING', 'SYSTEME', 'Type initial organisation', TRUE, TRUE, TRUE, NOW(), 'system', 0), - (gen_random_uuid(), 'defaut.utilisateur.systeme', 'system', 'STRING', 'SYSTEME', 'Identifiant utilisateur système', FALSE, FALSE, TRUE, NOW(), 'system', 0), - (gen_random_uuid(), 'defaut.montant.cotisation', '0', 'NUMBER', 'SYSTEME', 'Montant cotisation par défaut', TRUE, TRUE, TRUE, NOW(), 'system', 0) -ON CONFLICT DO NOTHING; - --- ───────────────────────────────────────────────────── --- Cat.7 — Index composites pour requêtes fréquentes --- ───────────────────────────────────────────────────── --- Aligner paiements avec l'entité (statut → statut_paiement si la colonne existe) -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'paiements' AND column_name = 'statut') - AND NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'paiements' AND column_name = 'statut_paiement') THEN - ALTER TABLE paiements RENAME COLUMN statut TO statut_paiement; - END IF; -END $$; - -CREATE INDEX IF NOT EXISTS idx_cotisation_org_statut_annee - ON cotisations (organisation_id, statut, annee); -CREATE INDEX IF NOT EXISTS idx_cotisation_membre_statut - ON cotisations (membre_id, statut); -CREATE INDEX IF NOT EXISTS idx_paiement_membre_statut_date - ON paiements (membre_id, statut_paiement, - date_paiement); -CREATE INDEX IF NOT EXISTS idx_notification_membre_statut - ON notifications (membre_id, statut, date_envoi); -CREATE INDEX IF NOT EXISTS idx_adhesion_org_statut - ON demandes_adhesion (organisation_id, statut); -CREATE INDEX IF NOT EXISTS idx_aide_org_statut_urgence - ON demandes_aide (organisation_id, statut, urgence); -CREATE INDEX IF NOT EXISTS idx_membreorg_org_statut - ON membres_organisations - (organisation_id, statut_membre); -CREATE INDEX IF NOT EXISTS idx_evenement_org_date_statut - ON evenements - (organisation_id, date_debut, statut); - --- ───────────────────────────────────────────────────── --- Cat.7 — Contraintes CHECK métier --- ───────────────────────────────────────────────────── -ALTER TABLE cotisations - ADD CONSTRAINT chk_montant_paye_le_du - CHECK (montant_paye <= montant_du); -ALTER TABLE souscriptions_organisation - ADD CONSTRAINT chk_quota_utilise_le_max - CHECK (quota_utilise <= quota_max); - - --- ========== V3.1__Add_Module_Disponible_FK.sql ========== --- ===================================================== --- V3.1 — Correction Intégrité Référentielle Modules --- Cat.2 — ModuleOrganisationActif -> ModuleDisponible --- ===================================================== - --- 1. Ajout de la colonne FK -ALTER TABLE modules_organisation_actifs - ADD COLUMN IF NOT EXISTS module_disponible_id UUID; - --- 2. Migration des données basées sur module_code -UPDATE modules_organisation_actifs moa -SET module_disponible_id = (SELECT id FROM modules_disponibles md WHERE md.code = moa.module_code); - --- 3. Ajout de la contrainte FK -ALTER TABLE modules_organisation_actifs - ADD CONSTRAINT fk_moa_module_disponible - FOREIGN KEY (module_disponible_id) REFERENCES modules_disponibles(id) - ON DELETE RESTRICT; - --- 4. Nettoyage (Optionnel : on garde module_code pour compatibilité DTO existante si nécessaire, --- mais on force la cohérence via un index unique si possible) -CREATE INDEX IF NOT EXISTS idx_moa_module_id ON modules_organisation_actifs(module_disponible_id); - --- Note: L'audit demandait l'intégrité, c'est fait. - - --- ========== V3.2__Seed_Types_Reference.sql ========== --- ===================================================== --- V3.2 — Initialisation des Types de Référence --- Cat.1 — Centralisation des domaines de valeurs --- Colonnes alignées sur l'entité TypeReference (domaine, code, etc.) --- ===================================================== - --- 2. Statut Matrimonial (complément éventuel à V3.0) -INSERT INTO types_reference (id, domaine, code, libelle, description, valeur_systeme, cree_par, actif, ordre, version, date_creation, est_defaut, est_systeme, ordre_affichage) -VALUES -(gen_random_uuid(), 'STATUT_MATRIMONIAL', 'CELIBATAIRE', 'Célibataire', 'Membre non marié', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'STATUT_MATRIMONIAL', 'MARIE', 'Marié(e)', 'Membre marié', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'STATUT_MATRIMONIAL', 'VEUF', 'Veuf/Veuve', 'Membre ayant perdu son conjoint', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'STATUT_MATRIMONIAL', 'DIVORCE', 'Divorcé(e)', 'Membre divorcé', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0) -ON CONFLICT (domaine, code) DO NOTHING; - --- 3. Type d'Identité -INSERT INTO types_reference (id, domaine, code, libelle, description, valeur_systeme, cree_par, actif, ordre, version, date_creation, est_defaut, est_systeme, ordre_affichage) -VALUES -(gen_random_uuid(), 'TYPE_IDENTITE', 'CNI', 'Carte Nationale d''Identité', 'Pièce d''identité nationale', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'TYPE_IDENTITE', 'PASSEPORT', 'Passeport', 'Passeport international', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'TYPE_IDENTITE', 'PERMIS_CONDUIRE', 'Permis de conduire', 'Permis de conduire officiel', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'TYPE_IDENTITE', 'CARTE_CONSULAIRE', 'Carte Consulaire', 'Carte délivrée par un consulat', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0) -ON CONFLICT (domaine, code) DO NOTHING; - --- 4. Objet de Paiement (compléments à V3.0) -INSERT INTO types_reference (id, domaine, code, libelle, description, valeur_systeme, cree_par, actif, ordre, version, date_creation, est_defaut, est_systeme, ordre_affichage) -VALUES -(gen_random_uuid(), 'OBJET_PAIEMENT', 'COTISATION', 'Cotisation annuelle', 'Paiement de la cotisation de membre', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'OBJET_PAIEMENT', 'DON', 'Don gracieux', 'Don volontaire pour l''association', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'OBJET_PAIEMENT', 'INSCRIPTION_EVENEMENT', 'Inscription à un événement', 'Paiement pour participer à un événement', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'OBJET_PAIEMENT', 'AMENDE', 'Amende / Sanction', 'Paiement suite à une sanction', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0) -ON CONFLICT (domaine, code) DO NOTHING; - --- 5. Type d'Organisation -INSERT INTO types_reference (id, domaine, code, libelle, description, valeur_systeme, cree_par, actif, ordre, version, date_creation, est_defaut, est_systeme, ordre_affichage) -VALUES -(gen_random_uuid(), 'TYPE_ORGANISATION', 'ASSOCIATION', 'Association', 'Organisation type association', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'TYPE_ORGANISATION', 'COOPERATIVE', 'Coopérative', 'Organisation type coopérative', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'TYPE_ORGANISATION', 'FEDERATION', 'Fédération', 'Regroupement d''associations', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'TYPE_ORGANISATION', 'CELLULE', 'Cellule de base', 'Unité locale d''une organisation', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0) -ON CONFLICT (domaine, code) DO NOTHING; - --- 6. Type de Rôle -INSERT INTO types_reference (id, domaine, code, libelle, description, valeur_systeme, cree_par, actif, ordre, version, date_creation, est_defaut, est_systeme, ordre_affichage) -VALUES -(gen_random_uuid(), 'TYPE_ROLE', 'SYSTEME', 'Système', 'Rôle global non modifiable', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'TYPE_ROLE', 'ORGANISATION', 'Organisation', 'Rôle spécifique à une organisation', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'TYPE_ROLE', 'PERSONNALISE', 'Personnalisé', 'Rôle créé manuellement', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0) -ON CONFLICT (domaine, code) DO NOTHING; - --- 7. Statut d'Inscription -INSERT INTO types_reference (id, domaine, code, libelle, description, valeur_systeme, cree_par, actif, ordre, version, date_creation, est_defaut, est_systeme, ordre_affichage) -VALUES -(gen_random_uuid(), 'STATUT_INSCRIPTION', 'CONFIRMEE', 'Confirmée', 'Inscription validée', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'STATUT_INSCRIPTION', 'EN_ATTENTE', 'En attente', 'En attente de validation', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'STATUT_INSCRIPTION', 'ANNULEE', 'Annulée', 'Inscription annulée par l''utilisateur', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0), -(gen_random_uuid(), 'STATUT_INSCRIPTION', 'REFUSEE', 'Refusée', 'Inscription rejetée par l''organisateur', TRUE, 'system', TRUE, 0, 0, NOW(), FALSE, TRUE, 0) -ON CONFLICT (domaine, code) DO NOTHING; - - --- ========== V3.3__Optimisation_Index_Performance.sql ========== --- ===================================================== --- V3.3 — Optimisation des Index de Performance --- Cat.7 — Index composites pour recherches fréquentes --- ===================================================== - --- 1. Index composite sur les membres (Recherche par nom complet) -CREATE INDEX IF NOT EXISTS idx_membre_nom_prenom ON utilisateurs(nom, prenom); - --- 2. Index composite sur les cotisations (Recherche par membre et année) -CREATE INDEX IF NOT EXISTS idx_cotisation_membre_annee ON cotisations(membre_id, annee); - --- 3. Index sur le Keycloak ID pour synchronisation rapide -CREATE INDEX IF NOT EXISTS idx_membre_keycloak_id ON utilisateurs(keycloak_id); - --- 4. Index sur le statut des paiements -CREATE INDEX IF NOT EXISTS idx_paiement_statut_paiement ON paiements(statut_paiement); - --- 5. Index sur les dates de création pour tris par défaut -CREATE INDEX IF NOT EXISTS idx_membre_date_creation ON utilisateurs(date_creation DESC); -CREATE INDEX IF NOT EXISTS idx_organisation_date_creation ON organisations(date_creation DESC); - - --- ========== V3.4__LCB_FT_Anti_Blanchiment.sql ========== --- ============================================================ --- V3.4 — LCB-FT / Anti-blanchiment (mutuelles) --- Spec: specs/001-mutuelles-anti-blanchiment/spec.md --- Traçabilité origine des fonds, KYC, seuils --- ============================================================ - --- 1. Utilisateurs (identité) — vigilance KYC -ALTER TABLE utilisateurs - ADD COLUMN IF NOT EXISTS niveau_vigilance_kyc VARCHAR(20) DEFAULT 'SIMPLIFIE', - ADD COLUMN IF NOT EXISTS statut_kyc VARCHAR(20) DEFAULT 'NON_VERIFIE', - ADD COLUMN IF NOT EXISTS date_verification_identite DATE; - -ALTER TABLE utilisateurs - ADD CONSTRAINT chk_utilisateur_niveau_kyc - CHECK (niveau_vigilance_kyc IS NULL OR niveau_vigilance_kyc IN ('SIMPLIFIE', 'RENFORCE')); -ALTER TABLE utilisateurs - ADD CONSTRAINT chk_utilisateur_statut_kyc - CHECK (statut_kyc IS NULL OR statut_kyc IN ('NON_VERIFIE', 'EN_COURS', 'VERIFIE', 'REFUSE')); - -CREATE INDEX IF NOT EXISTS idx_utilisateur_statut_kyc ON utilisateurs(statut_kyc); - -COMMENT ON COLUMN utilisateurs.niveau_vigilance_kyc IS 'Niveau de vigilance KYC LCB-FT'; -COMMENT ON COLUMN utilisateurs.statut_kyc IS 'Statut vérification identité'; -COMMENT ON COLUMN utilisateurs.date_verification_identite IS 'Date de dernière vérification d''identité'; - --- 2. Intentions de paiement — origine des fonds / justification LCB-FT -ALTER TABLE intentions_paiement - ADD COLUMN IF NOT EXISTS origine_fonds VARCHAR(200), - ADD COLUMN IF NOT EXISTS justification_lcb_ft TEXT; - -COMMENT ON COLUMN intentions_paiement.origine_fonds IS 'Origine des fonds déclarée (obligatoire au-dessus du seuil)'; -COMMENT ON COLUMN intentions_paiement.justification_lcb_ft IS 'Justification LCB-FT optionnelle'; - --- 3. Transactions épargne — origine des fonds, pièce justificative (si la table existe) -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'transactions_epargne') THEN - ALTER TABLE transactions_epargne - ADD COLUMN IF NOT EXISTS origine_fonds VARCHAR(200), - ADD COLUMN IF NOT EXISTS piece_justificative_id UUID; - EXECUTE 'COMMENT ON COLUMN transactions_epargne.origine_fonds IS ''Origine des fonds (obligatoire au-dessus du seuil LCB-FT)'''; - EXECUTE 'COMMENT ON COLUMN transactions_epargne.piece_justificative_id IS ''Référence pièce jointe justificative'''; - END IF; -END $$; - --- 4. Paramètres LCB-FT (seuils par organisation ou globaux) -CREATE TABLE IF NOT EXISTS parametres_lcb_ft ( +-- Table tontines +CREATE TABLE IF NOT EXISTS tontines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - organisation_id UUID, - code_devise VARCHAR(3) NOT NULL DEFAULT 'XOF', - montant_seuil_justification DECIMAL(18,4) NOT NULL, - montant_seuil_validation_manuelle DECIMAL(18,4), - actif BOOLEAN NOT NULL DEFAULT TRUE, - date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + nom VARCHAR(200) NOT NULL, + description TEXT, + organisation_id UUID REFERENCES organisations(id), + montant_cotisation DECIMAL(12,2) NOT NULL, + periodicite VARCHAR(30), + nombre_membres_max INTEGER, + statut VARCHAR(30) DEFAULT 'ACTIVE', + date_debut DATE, + date_fin DATE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), date_modification TIMESTAMP, cree_par VARCHAR(255), modifie_par VARCHAR(255), - version BIGINT NOT NULL DEFAULT 0, - CONSTRAINT fk_param_lcb_ft_org FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE, - CONSTRAINT chk_param_devise CHECK (code_devise ~ '^[A-Z]{3}$') + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_param_lcb_ft_org_devise - ON parametres_lcb_ft(COALESCE(organisation_id, '00000000-0000-0000-0000-000000000000'::uuid), code_devise); -CREATE INDEX IF NOT EXISTS idx_param_lcb_ft_org ON parametres_lcb_ft(organisation_id); +-- Table tours_tontine +CREATE TABLE IF NOT EXISTS tours_tontine ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tontine_id UUID NOT NULL REFERENCES tontines(id) ON DELETE CASCADE, + membre_id UUID REFERENCES utilisateurs(id), + numero_tour INTEGER NOT NULL, + date_tour DATE, + montant_recu DECIMAL(12,2), + statut VARCHAR(30) DEFAULT 'EN_ATTENTE', + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); -COMMENT ON TABLE parametres_lcb_ft IS 'Seuils LCB-FT : au-dessus de montant_seuil_justification, origine des fonds obligatoire'; -COMMENT ON COLUMN parametres_lcb_ft.organisation_id IS 'NULL = paramètres plateforme par défaut'; +-- Table campagnes_vote +CREATE TABLE IF NOT EXISTS campagnes_vote ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + titre VARCHAR(255) NOT NULL, + description TEXT, + organisation_id UUID REFERENCES organisations(id), + date_debut TIMESTAMP, + date_fin TIMESTAMP, + statut VARCHAR(30) DEFAULT 'PLANIFIEE', + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); --- Valeur par défaut plateforme (XOF) — une seule ligne org NULL + XOF (toutes colonnes NOT NULL fournies) -INSERT INTO parametres_lcb_ft (id, organisation_id, code_devise, montant_seuil_justification, montant_seuil_validation_manuelle, cree_par, actif, date_creation, version) -SELECT gen_random_uuid(), NULL, 'XOF', 500000, 1000000, 'system', TRUE, NOW(), 0 -WHERE NOT EXISTS (SELECT 1 FROM parametres_lcb_ft WHERE organisation_id IS NULL AND code_devise = 'XOF'); +-- Table candidats +CREATE TABLE IF NOT EXISTS candidats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + campagne_id UUID NOT NULL REFERENCES campagnes_vote(id) ON DELETE CASCADE, + membre_id UUID NOT NULL REFERENCES utilisateurs(id), + poste VARCHAR(100), + programme TEXT, + nb_voix INTEGER DEFAULT 0, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); +-- Table campagnes_collecte +CREATE TABLE IF NOT EXISTS campagnes_collecte ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + titre VARCHAR(255) NOT NULL, + description TEXT, + objectif_montant DECIMAL(14,2), + montant_collecte DECIMAL(14,2) DEFAULT 0, + organisation_id UUID REFERENCES organisations(id), + date_debut DATE, + date_fin DATE, + statut VARCHAR(30) DEFAULT 'ACTIVE', + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); --- ========== V3.5__Add_Organisation_Address_Fields.sql ========== --- Migration V3.5 : Ajout des champs d'adresse dans la table organisations --- Date : 2026-02-28 --- Description : Ajoute les champs adresse, ville, région, pays et code postal --- pour stocker l'adresse principale directement dans organisations +-- Table contributions_collecte +CREATE TABLE IF NOT EXISTS contributions_collecte ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + campagne_id UUID NOT NULL REFERENCES campagnes_collecte(id) ON DELETE CASCADE, + membre_id UUID REFERENCES utilisateurs(id), + montant DECIMAL(12,2) NOT NULL, + date_contribution TIMESTAMP DEFAULT NOW(), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); --- Ajout des colonnes d'adresse -ALTER TABLE organisations ADD COLUMN IF NOT EXISTS adresse VARCHAR(500); -ALTER TABLE organisations ADD COLUMN IF NOT EXISTS ville VARCHAR(100); -ALTER TABLE organisations ADD COLUMN IF NOT EXISTS region VARCHAR(100); -ALTER TABLE organisations ADD COLUMN IF NOT EXISTS pays VARCHAR(100); -ALTER TABLE organisations ADD COLUMN IF NOT EXISTS code_postal VARCHAR(20); +-- Table campagnes_agricoles +CREATE TABLE IF NOT EXISTS campagnes_agricoles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom VARCHAR(200) NOT NULL, + description TEXT, + organisation_id UUID REFERENCES organisations(id), + culture VARCHAR(100), + superficie_hectares DECIMAL(10,2), + date_debut DATE, + date_fin DATE, + statut VARCHAR(30) DEFAULT 'PLANIFIEE', + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); --- Ajout d'index pour optimiser les recherches par localisation -CREATE INDEX IF NOT EXISTS idx_organisation_ville ON organisations(ville); -CREATE INDEX IF NOT EXISTS idx_organisation_region ON organisations(region); -CREATE INDEX IF NOT EXISTS idx_organisation_pays ON organisations(pays); +-- Table projets_ong +CREATE TABLE IF NOT EXISTS projets_ong ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom VARCHAR(200) NOT NULL, + description TEXT, + organisation_id UUID REFERENCES organisations(id), + budget DECIMAL(14,2), + date_debut DATE, + date_fin DATE, + statut VARCHAR(30) DEFAULT 'EN_COURS', + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); --- Commentaires sur les colonnes -COMMENT ON COLUMN organisations.adresse IS 'Adresse principale de l''organisation (dénormalisée pour performance)'; -COMMENT ON COLUMN organisations.ville IS 'Ville de l''adresse principale'; -COMMENT ON COLUMN organisations.region IS 'Région/Province/État de l''adresse principale'; -COMMENT ON COLUMN organisations.pays IS 'Pays de l''adresse principale'; -COMMENT ON COLUMN organisations.code_postal IS 'Code postal de l''adresse principale'; +-- Table dons_religieux +CREATE TABLE IF NOT EXISTS dons_religieux ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + membre_id UUID REFERENCES utilisateurs(id), + organisation_id UUID REFERENCES organisations(id), + montant DECIMAL(12,2) NOT NULL, + type_don VARCHAR(50), + date_don DATE DEFAULT CURRENT_DATE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); +-- Table echelons_organigramme +CREATE TABLE IF NOT EXISTS echelons_organigramme ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom VARCHAR(100) NOT NULL, + niveau INTEGER NOT NULL, + organisation_id UUID REFERENCES organisations(id), + parent_id UUID REFERENCES echelons_organigramme(id), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); --- ========== V3.6__Create_Test_Organisations.sql ========== --- Migration V3.6 - Création des organisations de test MUKEFI et MESKA --- UnionFlow - Configuration initiale pour tests --- ⚠ Correction : INSERT dans "organisations" (pluriel, table JPA gérée par Hibernate, --- définie en V1.2), et non "organisation" (singulier, ancienne table isolée). - --- ============================================================================ --- 1. ORGANISATION MUKEFI (Mutuelle d'épargne et de crédit) --- ============================================================================ - -DELETE FROM organisations WHERE nom_court = 'MUKEFI'; - -INSERT INTO organisations ( - id, - nom, - nom_court, - description, - email, - telephone, - site_web, - type_organisation, - statut, - date_fondation, - numero_enregistrement, - devise, - budget_annuel, - cotisation_obligatoire, - montant_cotisation_annuelle, - objectifs, - activites_principales, - partenaires, - latitude, - longitude, - date_creation, - date_modification, - cree_par, - modifie_par, - version, - actif, - accepte_nouveaux_membres, - est_organisation_racine, - niveau_hierarchique, - nombre_membres, - nombre_administrateurs, - organisation_publique -) VALUES ( - gen_random_uuid(), - 'Mutuelle d''Épargne et de Crédit des Fonctionnaires et Indépendants', - 'MUKEFI', - 'Mutuelle d''épargne et de crédit dédiée aux fonctionnaires et travailleurs indépendants de Côte d''Ivoire', - 'contact@mukefi.org', - '+225 07 00 00 00 01', - 'https://mukefi.org', - 'ASSOCIATION', - 'ACTIVE', - '2020-01-15', - 'MUT-CI-2020-001', - 'XOF', - 500000000, - true, - 50000, - 'Favoriser l''épargne et l''accès au crédit pour les membres', - 'Épargne, crédit, micro-crédit, formation financière', - 'Banque Centrale des États de l''Afrique de l''Ouest (BCEAO)', - 5.3364, - -4.0267, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - 'superadmin@unionflow.test', - 'superadmin@unionflow.test', - 0, - true, - true, - true, - 0, - 0, - 0, - true +-- Table agrements_professionnels +CREATE TABLE IF NOT EXISTS agrements_professionnels ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + membre_id UUID NOT NULL REFERENCES utilisateurs(id), + profession VARCHAR(200), + numero_agrement VARCHAR(100), + organisme_delivrance VARCHAR(255), + date_delivrance DATE, + date_expiration DATE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -- ============================================================================ --- 2. ORGANISATION MESKA (Association) +-- 11. TABLES LCB-FT (Lutte Contre le Blanchiment) -- ============================================================================ -DELETE FROM organisations WHERE nom_court = 'MESKA'; - -INSERT INTO organisations ( - id, - nom, - nom_court, - description, - email, - telephone, - site_web, - type_organisation, - statut, - date_fondation, - numero_enregistrement, - devise, - budget_annuel, - cotisation_obligatoire, - montant_cotisation_annuelle, - objectifs, - activites_principales, - partenaires, - latitude, - longitude, - date_creation, - date_modification, - cree_par, - modifie_par, - version, - actif, - accepte_nouveaux_membres, - est_organisation_racine, - niveau_hierarchique, - nombre_membres, - nombre_administrateurs, - organisation_publique -) VALUES ( - gen_random_uuid(), - 'Mouvement d''Entraide et de Solidarité de Koumassi et Adjamé', - 'MESKA', - 'Association communautaire d''entraide et de solidarité basée à Abidjan', - 'contact@meska.org', - '+225 07 00 00 00 02', - 'https://meska.org', - 'ASSOCIATION', - 'ACTIVE', - '2018-06-20', - 'ASSO-CI-2018-045', - 'XOF', - 25000000, - true, - 25000, - 'Promouvoir la solidarité et l''entraide entre les membres des communes de Koumassi et Adjamé', - 'Aide sociale, événements communautaires, formations, projets collectifs', - 'Mairie de Koumassi, Mairie d''Adjamé', - 5.2931, - -3.9468, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP, - 'superadmin@unionflow.test', - 'superadmin@unionflow.test', - 0, - true, - true, - true, - 0, - 0, - 0, - true +-- Table parametres_lcb_ft +CREATE TABLE IF NOT EXISTS parametres_lcb_ft ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + seuil_declaration DECIMAL(14,2) DEFAULT 500000, + seuil_vigilance_renforcee DECIMAL(14,2) DEFAULT 1000000, + duree_conservation_jours INTEGER DEFAULT 1825, + organisation_id UUID REFERENCES organisations(id), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); +-- Table alertes_lcb_ft +CREATE TABLE IF NOT EXISTS alertes_lcb_ft ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type_alerte VARCHAR(50) NOT NULL, + severite VARCHAR(20) NOT NULL, + organisation_id UUID NOT NULL REFERENCES organisations(id) ON DELETE CASCADE, + membre_id UUID REFERENCES utilisateurs(id) ON DELETE SET NULL, + transaction_id UUID, + montant DECIMAL(14,2), + description TEXT, + statut VARCHAR(30) DEFAULT 'NOUVELLE', + date_alerte TIMESTAMP DEFAULT NOW(), + traite_par UUID REFERENCES utilisateurs(id), + date_traitement TIMESTAMP, + commentaire TEXT, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE INDEX idx_alertes_lcb_statut ON alertes_lcb_ft(statut); +CREATE INDEX idx_alertes_lcb_organisation ON alertes_lcb_ft(organisation_id); +CREATE INDEX idx_alertes_lcb_membre ON alertes_lcb_ft(membre_id); --- ========== V3.7__Seed_Test_Members.sql ========== -- ============================================================================ --- V3.7 — Données de test : Membres et Cotisations --- Tables cibles : --- utilisateurs -> entité JPA Membre --- organisations -> entité JPA Organisation (V1.2) --- membres_organisations -> jointure membre <> organisation --- cotisations -> entité JPA Cotisation +-- 12. TABLES DIVERS -- ============================================================================ --- ───────────────────────────────────────────────────────────────────────────── --- 0. Nettoyage (idempotent) --- ───────────────────────────────────────────────────────────────────────────── - -DELETE FROM cotisations -WHERE membre_id IN ( - SELECT id FROM utilisateurs - WHERE email IN ( - 'membre.mukefi@unionflow.test', - 'admin.mukefi@unionflow.test', - 'membre.meska@unionflow.test' - ) +-- Table adresses +CREATE TABLE IF NOT EXISTS adresses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + entite_type VARCHAR(50) NOT NULL, + entite_id UUID NOT NULL, + type_adresse VARCHAR(30), + adresse_ligne1 VARCHAR(255), + adresse_ligne2 VARCHAR(255), + ville VARCHAR(100), + code_postal VARCHAR(20), + region VARCHAR(100), + pays VARCHAR(100) DEFAULT 'Côte d''Ivoire', + latitude DECIMAL(9,6), + longitude DECIMAL(9,6), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -DELETE FROM membres_organisations -WHERE utilisateur_id IN ( - SELECT id FROM utilisateurs - WHERE email IN ( - 'membre.mukefi@unionflow.test', - 'admin.mukefi@unionflow.test', - 'membre.meska@unionflow.test' - ) +-- Table ayants_droit +CREATE TABLE IF NOT EXISTS ayants_droit ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + membre_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, + nom VARCHAR(100) NOT NULL, + prenom VARCHAR(100) NOT NULL, + lien_parente VARCHAR(50), + date_naissance DATE, + telephone VARCHAR(30), + pourcentage_part DECIMAL(5,2), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -DELETE FROM utilisateurs -WHERE email IN ( - 'membre.mukefi@unionflow.test', - 'admin.mukefi@unionflow.test', - 'membre.meska@unionflow.test' +-- Table types_reference +CREATE TABLE IF NOT EXISTS types_reference ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(50) NOT NULL, + categorie VARCHAR(50) NOT NULL, + libelle VARCHAR(200) NOT NULL, + description TEXT, + ordre INTEGER DEFAULT 0, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE, + CONSTRAINT uk_type_ref UNIQUE (categorie, code) ); --- ───────────────────────────────────────────────────────────────────────────── --- 0b. S'assurer que MUKEFI et MESKA existent dans "organisations" (table JPA). --- Si V3.6 les a déjà insérées, ON CONFLICT (email) DO NOTHING évite le doublon. --- ───────────────────────────────────────────────────────────────────────────── - -INSERT INTO organisations ( - id, nom, nom_court, type_organisation, statut, email, telephone, - site_web, date_fondation, numero_enregistrement, devise, - budget_annuel, cotisation_obligatoire, montant_cotisation_annuelle, - objectifs, activites_principales, partenaires, latitude, longitude, - date_creation, date_modification, cree_par, modifie_par, version, actif, - accepte_nouveaux_membres, est_organisation_racine, niveau_hierarchique, - nombre_membres, nombre_administrateurs, organisation_publique -) VALUES ( - gen_random_uuid(), - 'Mutuelle d''Épargne et de Crédit des Fonctionnaires et Indépendants', - 'MUKEFI', 'ASSOCIATION', 'ACTIVE', - 'contact@mukefi.org', '+225 07 00 00 00 01', 'https://mukefi.org', - '2020-01-15', 'MUT-CI-2020-001', 'XOF', - 500000000, true, 50000, - 'Favoriser l''épargne et l''accès au crédit pour les membres', - 'Épargne, crédit, micro-crédit, formation financière', - 'BCEAO', 5.3364, -4.0267, - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true, - true, true, 0, 0, 0, true -) ON CONFLICT (email) DO NOTHING; - -INSERT INTO organisations ( - id, nom, nom_court, type_organisation, statut, email, telephone, - site_web, date_fondation, numero_enregistrement, devise, - budget_annuel, cotisation_obligatoire, montant_cotisation_annuelle, - objectifs, activites_principales, partenaires, latitude, longitude, - date_creation, date_modification, cree_par, modifie_par, version, actif, - accepte_nouveaux_membres, est_organisation_racine, niveau_hierarchique, - nombre_membres, nombre_administrateurs, organisation_publique -) VALUES ( - gen_random_uuid(), - 'Mouvement d''Entraide et de Solidarité de Koumassi et Adjamé', - 'MESKA', 'ASSOCIATION', 'ACTIVE', - 'contact@meska.org', '+225 07 00 00 00 02', 'https://meska.org', - '2018-06-20', 'ASSO-CI-2018-045', 'XOF', - 25000000, true, 25000, - 'Promouvoir la solidarité et l''entraide entre les membres des communes de Koumassi et Adjamé', - 'Aide sociale, événements communautaires, formations, projets collectifs', - 'Mairie de Koumassi, Mairie d''Adjamé', 5.2931, -3.9468, - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true, - true, true, 0, 0, 0, true -) ON CONFLICT (email) DO NOTHING; - --- ───────────────────────────────────────────────────────────────────────────── --- 1. MEMBRE : membre.mukefi@unionflow.test (MUKEFI) --- ───────────────────────────────────────────────────────────────────────────── - -INSERT INTO utilisateurs ( - id, numero_membre, prenom, nom, email, telephone, date_naissance, - nationalite, profession, statut_compte, - date_creation, date_modification, cree_par, modifie_par, version, actif -) VALUES ( - gen_random_uuid(), 'MBR-MUKEFI-001', 'Membre', 'MUKEFI', - 'membre.mukefi@unionflow.test', '+22507000101', '1985-06-15', - 'Ivoirien', 'Fonctionnaire', 'ACTIF', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true +-- Table modules_organisation_actifs +CREATE TABLE IF NOT EXISTS modules_organisation_actifs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id UUID NOT NULL REFERENCES organisations(id) ON DELETE CASCADE, + code_module VARCHAR(50) NOT NULL, + actif BOOLEAN DEFAULT TRUE, + date_activation DATE, + date_desactivation DATE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + CONSTRAINT uk_org_module UNIQUE (organisation_id, code_module) ); --- ───────────────────────────────────────────────────────────────────────────── --- 2. MEMBRE : admin.mukefi@unionflow.test (admin MUKEFI) --- ───────────────────────────────────────────────────────────────────────────── - -INSERT INTO utilisateurs ( - id, numero_membre, prenom, nom, email, telephone, date_naissance, - nationalite, profession, statut_compte, - date_creation, date_modification, cree_par, modifie_par, version, actif -) VALUES ( - gen_random_uuid(), 'MBR-MUKEFI-ADMIN', 'Admin', 'MUKEFI', - 'admin.mukefi@unionflow.test', '+22507000102', '1978-04-22', - 'Ivoirien', 'Administrateur', 'ACTIF', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true +-- Table module_disponible (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS module_disponible ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(50) NOT NULL UNIQUE, + nom VARCHAR(200) NOT NULL, + description TEXT, + version VARCHAR(20), + actif BOOLEAN DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version_record BIGINT DEFAULT 0 ); --- ───────────────────────────────────────────────────────────────────────────── --- 3. MEMBRE : membre.meska@unionflow.test (MESKA) --- ───────────────────────────────────────────────────────────────────────────── - -INSERT INTO utilisateurs ( - id, numero_membre, prenom, nom, email, telephone, date_naissance, - nationalite, profession, statut_compte, - date_creation, date_modification, cree_par, modifie_par, version, actif -) VALUES ( - gen_random_uuid(), 'MBR-MESKA-001', 'Membre', 'MESKA', - 'membre.meska@unionflow.test', '+22507000201', '1990-11-30', - 'Ivoirienne', 'Commercante', 'ACTIF', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true +-- Table parametres_cotisation_organisation +CREATE TABLE IF NOT EXISTS parametres_cotisation_organisation ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id UUID NOT NULL REFERENCES organisations(id) ON DELETE CASCADE, + type_cotisation VARCHAR(50) NOT NULL, + montant_defaut DECIMAL(12,2), + recurrence VARCHAR(30), + obligatoire BOOLEAN DEFAULT FALSE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); --- ───────────────────────────────────────────────────────────────────────────── --- 4. RATTACHEMENTS membres_organisations --- ───────────────────────────────────────────────────────────────────────────── - -INSERT INTO membres_organisations ( - id, utilisateur_id, organisation_id, statut_membre, date_adhesion, - date_creation, date_modification, cree_par, modifie_par, version, actif -) VALUES ( - gen_random_uuid(), - (SELECT id FROM utilisateurs WHERE email = 'membre.mukefi@unionflow.test'), - (SELECT id FROM organisations WHERE nom_court = 'MUKEFI' LIMIT 1), - 'ACTIF', '2020-03-01', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true +-- Table comptes_wave (intégration Wave) +CREATE TABLE IF NOT EXISTS comptes_wave ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id UUID REFERENCES organisations(id), + numero_compte_wave VARCHAR(50) UNIQUE, + nom_titulaire VARCHAR(200), + solde DECIMAL(14,2) DEFAULT 0, + actif BOOLEAN DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0 ); -INSERT INTO membres_organisations ( - id, utilisateur_id, organisation_id, statut_membre, date_adhesion, - date_creation, date_modification, cree_par, modifie_par, version, actif -) VALUES ( - gen_random_uuid(), - (SELECT id FROM utilisateurs WHERE email = 'admin.mukefi@unionflow.test'), - (SELECT id FROM organisations WHERE nom_court = 'MUKEFI' LIMIT 1), - 'ACTIF', '2020-01-15', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true +-- Table transaction_wave (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS transaction_wave ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + compte_wave_id UUID REFERENCES comptes_wave(id), + transaction_id_wave VARCHAR(100) UNIQUE, + type_transaction VARCHAR(50), + montant DECIMAL(14,2), + statut VARCHAR(30), + metadata TEXT, + date_transaction TIMESTAMP DEFAULT NOW(), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); -INSERT INTO membres_organisations ( - id, utilisateur_id, organisation_id, statut_membre, date_adhesion, - date_creation, date_modification, cree_par, modifie_par, version, actif -) VALUES ( - gen_random_uuid(), - (SELECT id FROM utilisateurs WHERE email = 'membre.meska@unionflow.test'), - (SELECT id FROM organisations WHERE nom_court = 'MESKA' LIMIT 1), - 'ACTIF', '2018-09-01', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true +-- Table webhooks_wave +CREATE TABLE IF NOT EXISTS webhooks_wave ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + event_type VARCHAR(50) NOT NULL, + payload TEXT, + signature VARCHAR(500), + traite BOOLEAN DEFAULT FALSE, + date_reception TIMESTAMP DEFAULT NOW(), + date_traitement TIMESTAMP, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); --- ───────────────────────────────────────────────────────────────────────────── --- 5. COTISATIONS pour membre.mukefi@unionflow.test --- ───────────────────────────────────────────────────────────────────────────── -ALTER TABLE cotisations ADD COLUMN IF NOT EXISTS libelle VARCHAR(500); - --- 2023 – PAYÉE -INSERT INTO cotisations ( - id, numero_reference, membre_id, organisation_id, - type_cotisation, libelle, montant_du, montant_paye, code_devise, - statut, date_echeance, date_paiement, annee, periode, - date_creation, date_modification, cree_par, modifie_par, version, actif, nombre_rappels, recurrente -) VALUES ( - gen_random_uuid(), 'COT-MUKEFI-2023-001', - (SELECT id FROM utilisateurs WHERE email = 'membre.mukefi@unionflow.test'), - (SELECT id FROM organisations WHERE nom_court = 'MUKEFI' LIMIT 1), - 'ANNUELLE', 'Cotisation annuelle 2023', 50000, 50000, 'XOF', - 'PAYEE', '2023-12-31', '2023-03-15 10:00:00', 2023, '2023', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true, 0, true +-- Table demande_adhesion (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS demande_adhesion ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom VARCHAR(100) NOT NULL, + prenom VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL, + telephone VARCHAR(30), + organisation_id UUID REFERENCES organisations(id), + statut VARCHAR(30) DEFAULT 'EN_ATTENTE', + date_demande TIMESTAMP DEFAULT NOW(), + date_traitement TIMESTAMP, + commentaire TEXT, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); --- 2024 – PAYÉE -INSERT INTO cotisations ( - id, numero_reference, membre_id, organisation_id, - type_cotisation, libelle, montant_du, montant_paye, code_devise, - statut, date_echeance, date_paiement, annee, periode, - date_creation, date_modification, cree_par, modifie_par, version, actif, nombre_rappels, recurrente -) VALUES ( - gen_random_uuid(), 'COT-MUKEFI-2024-001', - (SELECT id FROM utilisateurs WHERE email = 'membre.mukefi@unionflow.test'), - (SELECT id FROM organisations WHERE nom_court = 'MUKEFI' LIMIT 1), - 'ANNUELLE', 'Cotisation annuelle 2024', 50000, 50000, 'XOF', - 'PAYEE', '2024-12-31', '2024-02-20 09:30:00', 2024, '2024', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true, 0, true +-- Table formule_abonnement (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS formule_abonnement ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nom VARCHAR(200) NOT NULL, + code VARCHAR(50) UNIQUE, + description TEXT, + prix_mensuel DECIMAL(12,2), + prix_annuel DECIMAL(12,2), + features TEXT, + actif BOOLEAN DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0 ); --- 2025 – EN ATTENTE -INSERT INTO cotisations ( - id, numero_reference, membre_id, organisation_id, - type_cotisation, libelle, montant_du, montant_paye, code_devise, - statut, date_echeance, annee, periode, - date_creation, date_modification, cree_par, modifie_par, version, actif, nombre_rappels, recurrente -) VALUES ( - gen_random_uuid(), 'COT-MUKEFI-2025-001', - (SELECT id FROM utilisateurs WHERE email = 'membre.mukefi@unionflow.test'), - (SELECT id FROM organisations WHERE nom_court = 'MUKEFI' LIMIT 1), - 'ANNUELLE', 'Cotisation annuelle 2025', 50000, 0, 'XOF', - 'EN_ATTENTE', '2025-12-31', 2025, '2025', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true, 0, true +-- Table souscription_organisation (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS souscription_organisation ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id UUID NOT NULL REFERENCES organisations(id) ON DELETE CASCADE, + formule_id UUID REFERENCES formule_abonnement(id), + date_debut DATE NOT NULL, + date_fin DATE, + statut VARCHAR(30) DEFAULT 'ACTIVE', + montant_paye DECIMAL(12,2), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE ); --- ───────────────────────────────────────────────────────────────────────────── --- 6. COTISATION pour membre.meska@unionflow.test --- ───────────────────────────────────────────────────────────────────────────── - -INSERT INTO cotisations ( - id, numero_reference, membre_id, organisation_id, - type_cotisation, libelle, montant_du, montant_paye, code_devise, - statut, date_echeance, date_paiement, annee, periode, - date_creation, date_modification, cree_par, modifie_par, version, actif, nombre_rappels, recurrente -) VALUES ( - gen_random_uuid(), 'COT-MESKA-2024-001', - (SELECT id FROM utilisateurs WHERE email = 'membre.meska@unionflow.test'), - (SELECT id FROM organisations WHERE nom_court = 'MESKA' LIMIT 1), - 'ANNUELLE', 'Cotisation annuelle 2024', 25000, 25000, 'XOF', - 'PAYEE', '2024-12-31', '2024-01-10 14:00:00', 2024, '2024', - CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'system', 'system', 0, true, 0, true +-- Table membre_suivi (réseau social) +CREATE TABLE IF NOT EXISTS membre_suivi ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + suiveur_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, + suivi_id UUID NOT NULL REFERENCES utilisateurs(id) ON DELETE CASCADE, + date_suivi TIMESTAMP DEFAULT NOW(), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + actif BOOLEAN NOT NULL DEFAULT TRUE, + CONSTRAINT uk_suiveur_suivi UNIQUE (suiveur_id, suivi_id), + CONSTRAINT chk_pas_auto_suivi CHECK (suiveur_id != suivi_id) ); +CREATE INDEX idx_membre_suivi_suiveur ON membre_suivi(suiveur_id); +CREATE INDEX idx_membre_suivi_suivi ON membre_suivi(suivi_id); + +-- Table validation_etape_demande (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS validation_etape_demande ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + demande_type VARCHAR(50) NOT NULL, + demande_id UUID NOT NULL, + etape INTEGER NOT NULL, + validateur_id UUID REFERENCES utilisateurs(id), + statut VARCHAR(30) DEFAULT 'EN_ATTENTE', + commentaire TEXT, + date_validation TIMESTAMP, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table compte_comptable (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS compte_comptable ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero VARCHAR(20) NOT NULL UNIQUE, + libelle VARCHAR(255) NOT NULL, + type_compte VARCHAR(30), + organisation_id UUID REFERENCES organisations(id), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table journal_comptable (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS journal_comptable ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(20) NOT NULL UNIQUE, + libelle VARCHAR(255) NOT NULL, + type_journal VARCHAR(30), + organisation_id UUID REFERENCES organisations(id), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table ecriture_comptable (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS ecriture_comptable ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + numero_piece VARCHAR(50) NOT NULL, + journal_id UUID REFERENCES journal_comptable(id), + date_ecriture DATE NOT NULL, + libelle VARCHAR(255), + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + date_modification TIMESTAMP, + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- Table ligne_ecriture (NOM CORRIGÉ dès le départ) +CREATE TABLE IF NOT EXISTS ligne_ecriture ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + ecriture_id UUID NOT NULL REFERENCES ecriture_comptable(id) ON DELETE CASCADE, + compte_id UUID NOT NULL REFERENCES compte_comptable(id), + libelle VARCHAR(255), + debit DECIMAL(14,2) DEFAULT 0, + credit DECIMAL(14,2) DEFAULT 0, + date_creation TIMESTAMP NOT NULL DEFAULT NOW(), + version BIGINT DEFAULT 0, + actif BOOLEAN NOT NULL DEFAULT TRUE +); + +-- ============================================================================ +-- COMMENTAIRES DE DOCUMENTATION +-- ============================================================================ + +COMMENT ON TABLE utilisateurs IS 'Table des membres/utilisateurs (entité: Membre)'; +COMMENT ON TABLE organisations IS 'Table des organisations (clubs, associations, coopératives)'; +COMMENT ON TABLE roles IS 'Table des rôles système'; +COMMENT ON TABLE permission IS 'Table des permissions système'; +COMMENT ON TABLE configuration IS 'Table de configuration système'; +COMMENT ON TABLE cotisations IS 'Table des cotisations des membres'; +COMMENT ON TABLE paiements IS 'Table des paiements'; +COMMENT ON TABLE comptes_epargne IS 'Table des comptes d''épargne'; +COMMENT ON TABLE demandes_credit IS 'Table des demandes de crédit'; +COMMENT ON TABLE evenements IS 'Table des événements'; +COMMENT ON TABLE demandes_aide IS 'Table des demandes d''aide solidarité'; +COMMENT ON TABLE ticket IS 'Table des tickets de support'; +COMMENT ON TABLE suggestion IS 'Table des suggestions des utilisateurs'; +COMMENT ON TABLE notifications IS 'Table des notifications'; +COMMENT ON TABLE document IS 'Table des documents'; +COMMENT ON TABLE alertes_lcb_ft IS 'Table des alertes anti-blanchiment'; +COMMENT ON TABLE audit_logs IS 'Table des logs d''audit'; + +-- ============================================================================ +-- FIN DU SCHEMA +-- ============================================================================ + +-- Afficher un résumé +DO $$ +DECLARE + table_count INTEGER; +BEGIN + SELECT COUNT(*) INTO table_count + FROM information_schema.tables + WHERE table_schema = 'public' AND table_type = 'BASE TABLE'; + + RAISE NOTICE '✅ Schema UnionFlow créé avec succès!'; + RAISE NOTICE 'Nombre total de tables: %', table_count; + RAISE NOTICE 'Date: %', NOW(); +END $$; diff --git a/src/main/resources/db/migration/V2__Add_Missing_BaseEntity_Columns.sql b/src/main/resources/db/migration/V2__Add_Missing_BaseEntity_Columns.sql new file mode 100644 index 0000000..2c4d9f1 --- /dev/null +++ b/src/main/resources/db/migration/V2__Add_Missing_BaseEntity_Columns.sql @@ -0,0 +1,219 @@ +-- ============================================================================ +-- V2: Ajout des colonnes BaseEntity manquantes +-- ============================================================================ +-- Auteur: Lions Dev +-- Date: 2026-03-16 +-- Description: Ajoute les colonnes cree_par et modifie_par dans toutes les +-- tables qui ne les ont pas encore. +-- Ces colonnes font partie de BaseEntity et sont requises par +-- Hibernate pour le bon fonctionnement de l'audit. +-- ============================================================================ + +-- Pattern: Pour chaque table sans cree_par/modifie_par, ajouter: +-- ALTER TABLE nom_table ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +-- ALTER TABLE nom_table ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +-- Tables de base +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE ayants_droit ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE ayants_droit ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE agrements_professionnels ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE agrements_professionnels ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE alertes_lcb_ft ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE alertes_lcb_ft ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE approver_actions ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE approver_actions ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE budget_lines ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE budget_lines ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE budgets ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE budgets ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE campagnes_agricoles ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE campagnes_agricoles ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE campagnes_collecte ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE campagnes_collecte ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE campagnes_vote ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE campagnes_vote ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE candidats ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE candidats ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE compte_comptable ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE compte_comptable ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE comptes_epargne ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE comptes_epargne ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE comptes_wave ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE comptes_wave ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE configuration ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE configuration ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE configuration_wave ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE configuration_wave ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE contributions_collecte ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE contributions_collecte ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE cotisations ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE cotisations ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE demande_adhesion ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE demande_adhesion ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE demandes_aide ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE demandes_aide ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE demandes_credit ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE demandes_credit ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE document ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE document ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE dons_religieux ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE dons_religieux ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE echeances_credit ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE echeances_credit ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE echelons_organigramme ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE echelons_organigramme ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE ecriture_comptable ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE ecriture_comptable ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE evenements ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE evenements ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE favori ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE favori ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE formule_abonnement ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE formule_abonnement ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE garanties_demande ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE garanties_demande ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE inscriptions_evenement ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE inscriptions_evenement ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE intention_paiement ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE intention_paiement ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE journal_comptable ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE journal_comptable ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE ligne_ecriture ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE ligne_ecriture ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE membre_organisation ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE membre_organisation ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE membre_role ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE membre_role ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE membre_suivi ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE membre_suivi ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE module_disponible ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE module_disponible ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE modules_organisation_actifs ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE modules_organisation_actifs ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE notifications ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE notifications ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE paiements ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE paiements ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE paiements_objets ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE paiements_objets ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE parametres_cotisation_organisation ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE parametres_cotisation_organisation ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE parametres_lcb_ft ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE parametres_lcb_ft ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE permission ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE permission ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE pieces_jointes ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE pieces_jointes ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE projets_ong ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE projets_ong ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE role_permission ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE role_permission ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE souscription_organisation ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE souscription_organisation ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE suggestion ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE suggestion ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE suggestion_vote ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE suggestion_vote ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE system_alerts ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE system_alerts ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE template_notification ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE template_notification ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE ticket ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE ticket ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE tontines ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE tontines ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE tours_tontine ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE tours_tontine ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE transaction_approvals ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE transaction_approvals ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE transaction_wave ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE transaction_wave ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE transactions_epargne ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE transactions_epargne ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE types_reference ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE types_reference ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE validation_etape_demande ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE validation_etape_demande ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE webhooks_wave ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE webhooks_wave ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +ALTER TABLE workflow_validation_config ADD COLUMN IF NOT EXISTS cree_par VARCHAR(255); +ALTER TABLE workflow_validation_config ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +-- Message de confirmation +DO $$ +BEGIN + RAISE NOTICE '✅ Colonnes BaseEntity ajoutées à toutes les tables'; +END $$; diff --git a/src/main/resources/db/migration/V3__Fix_Missing_Business_Columns.sql b/src/main/resources/db/migration/V3__Fix_Missing_Business_Columns.sql new file mode 100644 index 0000000..c194851 --- /dev/null +++ b/src/main/resources/db/migration/V3__Fix_Missing_Business_Columns.sql @@ -0,0 +1,92 @@ +-- ============================================================================ +-- V3: Correction des colonnes métier manquantes +-- ============================================================================ +-- Auteur: Lions Dev +-- Date: 2026-03-16 +-- Description: Ajoute les colonnes métier manquantes dans les tables +-- adresses et alert_configuration +-- ============================================================================ + +-- Table adresses - colonnes manquantes +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS adresse VARCHAR(500); +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS complement_adresse VARCHAR(200); +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS principale BOOLEAN DEFAULT false; +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS libelle VARCHAR(100); +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS notes VARCHAR(500); +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS organisation_id UUID; +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS membre_id UUID; +ALTER TABLE adresses ADD COLUMN IF NOT EXISTS evenement_id UUID; + +-- Ajouter NOT NULL après coup (si la colonne existe déjà, ça échouera silencieusement) +DO $$ +BEGIN + BEGIN + ALTER TABLE adresses ALTER COLUMN principale SET NOT NULL; + EXCEPTION WHEN OTHERS THEN + NULL; + END; +END $$; + +-- Index pour adresses (s'ils n'existent pas déjà) +CREATE INDEX IF NOT EXISTS idx_adresse_organisation ON adresses(organisation_id); +CREATE INDEX IF NOT EXISTS idx_adresse_membre ON adresses(membre_id); +CREATE INDEX IF NOT EXISTS idx_adresse_evenement ON adresses(evenement_id); + +-- Foreign keys pour adresses (avec gestion des doublons) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_adresse_organisation') THEN + ALTER TABLE adresses ADD CONSTRAINT fk_adresse_organisation + FOREIGN KEY (organisation_id) REFERENCES organisations(id) ON DELETE CASCADE; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_adresse_membre') THEN + ALTER TABLE adresses ADD CONSTRAINT fk_adresse_membre + FOREIGN KEY (membre_id) REFERENCES utilisateurs(id) ON DELETE CASCADE; + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_adresse_evenement') THEN + ALTER TABLE adresses ADD CONSTRAINT fk_adresse_evenement + FOREIGN KEY (evenement_id) REFERENCES evenements(id) ON DELETE CASCADE; + END IF; +END $$; + +-- Table alert_configuration - colonnes manquantes +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS cpu_high_alert_enabled BOOLEAN DEFAULT true; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS cpu_threshold_percent INTEGER DEFAULT 80; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS cpu_duration_minutes INTEGER DEFAULT 5; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS memory_low_alert_enabled BOOLEAN DEFAULT true; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS memory_threshold_percent INTEGER DEFAULT 85; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS critical_error_alert_enabled BOOLEAN DEFAULT true; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS error_alert_enabled BOOLEAN DEFAULT true; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS connection_failure_alert_enabled BOOLEAN DEFAULT true; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS connection_failure_threshold INTEGER DEFAULT 100; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS connection_failure_window_minutes INTEGER DEFAULT 5; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS email_notifications_enabled BOOLEAN DEFAULT true; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS push_notifications_enabled BOOLEAN DEFAULT false; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS sms_notifications_enabled BOOLEAN DEFAULT false; +ALTER TABLE alert_configuration ADD COLUMN IF NOT EXISTS alert_email_recipients VARCHAR(1000) DEFAULT 'admin@unionflow.test'; + +-- Ajouter NOT NULL après coup +DO $$ +BEGIN + BEGIN ALTER TABLE alert_configuration ALTER COLUMN cpu_high_alert_enabled SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN cpu_threshold_percent SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN cpu_duration_minutes SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN memory_low_alert_enabled SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN memory_threshold_percent SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN critical_error_alert_enabled SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN error_alert_enabled SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN connection_failure_alert_enabled SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN connection_failure_threshold SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN connection_failure_window_minutes SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN email_notifications_enabled SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN push_notifications_enabled SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; + BEGIN ALTER TABLE alert_configuration ALTER COLUMN sms_notifications_enabled SET NOT NULL; EXCEPTION WHEN OTHERS THEN NULL; END; +END $$; + +-- Message de confirmation +DO $$ +BEGIN + RAISE NOTICE '✅ Colonnes métier manquantes ajoutées'; +END $$; diff --git a/src/main/resources/db/migration/V4__Fix_SystemLogs_Table.sql b/src/main/resources/db/migration/V4__Fix_SystemLogs_Table.sql new file mode 100644 index 0000000..39c331f --- /dev/null +++ b/src/main/resources/db/migration/V4__Fix_SystemLogs_Table.sql @@ -0,0 +1,54 @@ +-- ============================================================================ +-- V4: Correction de la table system_logs +-- ============================================================================ +-- Auteur: Lions Dev +-- Date: 2026-03-16 +-- Description: Corrige les noms de colonnes et ajoute les colonnes manquantes +-- dans system_logs pour correspondre à l'entité JPA SystemLog +-- ============================================================================ + +-- 1. Renommer les colonnes avec des noms incorrects +ALTER TABLE system_logs RENAME COLUMN niveau TO level; +ALTER TABLE system_logs RENAME COLUMN stacktrace TO details; + +-- 2. Modifier utilisateur_id: UUID → VARCHAR(255) et renommer user_id +-- D'abord supprimer la colonne UUID, puis ajouter VARCHAR +ALTER TABLE system_logs DROP COLUMN IF EXISTS utilisateur_id; +ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS user_id VARCHAR(255); + +-- 3. Ajouter les colonnes manquantes +ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS timestamp TIMESTAMP; +ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS session_id VARCHAR(255); +ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS endpoint VARCHAR(500); +ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS http_status_code INTEGER; +ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS date_modification TIMESTAMP; +ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS modifie_par VARCHAR(255); + +-- 4. Ajuster les types de colonnes existantes +ALTER TABLE system_logs ALTER COLUMN message TYPE VARCHAR(1000); +ALTER TABLE system_logs ALTER COLUMN ip_address TYPE VARCHAR(45); + +-- 5. Définir timestamp NOT NULL après coup (si données existantes, timestamp = date_creation) +UPDATE system_logs SET timestamp = date_creation WHERE timestamp IS NULL; + +DO $$ +BEGIN + BEGIN + ALTER TABLE system_logs ALTER COLUMN timestamp SET NOT NULL; + EXCEPTION WHEN OTHERS THEN + NULL; + END; +END $$; + +-- 6. Recréer les index avec les bons noms de colonnes +DROP INDEX IF EXISTS idx_system_logs_niveau; +CREATE INDEX IF NOT EXISTS idx_system_log_level ON system_logs(level); +CREATE INDEX IF NOT EXISTS idx_system_log_timestamp ON system_logs(timestamp); +CREATE INDEX IF NOT EXISTS idx_system_log_source ON system_logs(source); +CREATE INDEX IF NOT EXISTS idx_system_log_user_id ON system_logs(user_id); + +-- Message de confirmation +DO $$ +BEGIN + RAISE NOTICE '✅ Table system_logs corrigée (colonnes renommées et ajoutées)'; +END $$; diff --git a/src/main/resources/db/migration/V5__Cleanup_AlertConfiguration_Obsolete_Columns.sql b/src/main/resources/db/migration/V5__Cleanup_AlertConfiguration_Obsolete_Columns.sql new file mode 100644 index 0000000..1ba9f69 --- /dev/null +++ b/src/main/resources/db/migration/V5__Cleanup_AlertConfiguration_Obsolete_Columns.sql @@ -0,0 +1,22 @@ +-- ============================================================================ +-- V5: Nettoyage des colonnes obsolètes dans alert_configuration +-- ============================================================================ +-- Auteur: Lions Dev +-- Date: 2026-03-16 +-- Description: Supprime les colonnes de V1 qui ne correspondent pas à +-- l'entité JPA AlertConfiguration (colonnes obsolètes) +-- ============================================================================ + +-- Supprimer les colonnes obsolètes de la version V1 d'alert_configuration +ALTER TABLE alert_configuration DROP COLUMN IF EXISTS type_alerte; +ALTER TABLE alert_configuration DROP COLUMN IF EXISTS seuil_critique; +ALTER TABLE alert_configuration DROP COLUMN IF EXISTS seuil_warning; +ALTER TABLE alert_configuration DROP COLUMN IF EXISTS notification_email; +ALTER TABLE alert_configuration DROP COLUMN IF EXISTS notification_sms; +ALTER TABLE alert_configuration DROP COLUMN IF EXISTS destinataires; + +-- Message de confirmation +DO $$ +BEGIN + RAISE NOTICE '✅ Colonnes obsolètes supprimées de alert_configuration'; +END $$; diff --git a/src/test/java/dev/lions/unionflow/server/exception/GlobalExceptionMapperTest.java b/src/test/java/dev/lions/unionflow/server/exception/GlobalExceptionMapperTest.java index 82db9d1..828a483 100644 --- a/src/test/java/dev/lions/unionflow/server/exception/GlobalExceptionMapperTest.java +++ b/src/test/java/dev/lions/unionflow/server/exception/GlobalExceptionMapperTest.java @@ -34,126 +34,123 @@ class GlobalExceptionMapperTest { @Test @DisplayName("RuntimeException générique → 500") void mapRuntimeException_otherRuntime_returns500() { - Response r = globalExceptionMapper.mapRuntimeException(new RuntimeException("inattendu")); + Response r = globalExceptionMapper.toResponse(new RuntimeException("inattendu")); assertThat(r.getStatus()).isEqualTo(500); assertThat(r.getEntity()).isNotNull(); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("error")).isEqualTo("Erreur interne"); + assertThat(body.get("error")).isEqualTo("Internal server error"); } @Test @DisplayName("IllegalArgumentException → 400") void mapRuntimeException_illegalArgument_returns400() { - Response r = globalExceptionMapper.mapRuntimeException(new IllegalArgumentException("critère manquant")); + Response r = globalExceptionMapper.toResponse(new IllegalArgumentException("critère manquant")); assertThat(r.getStatus()).isEqualTo(400); assertThat(r.getEntity()).isNotNull(); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("error")).isEqualTo("Requête invalide"); + assertThat(body.get("error")).isEqualTo("critère manquant"); } @Test - @DisplayName("IllegalStateException → 409") - void mapRuntimeException_illegalState_returns409() { - Response r = globalExceptionMapper.mapRuntimeException(new IllegalStateException("déjà existant")); - assertThat(r.getStatus()).isEqualTo(409); + @DisplayName("IllegalStateException → 400 (traité comme BadRequest)") + void mapRuntimeException_illegalState_returns400() { + Response r = globalExceptionMapper.toResponse(new IllegalStateException("déjà existant")); + assertThat(r.getStatus()).isEqualTo(400); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("error")).isEqualTo("Conflit"); + assertThat(body.get("error")).isEqualTo("déjà existant"); } @Test @DisplayName("NotFoundException → 404") void mapRuntimeException_notFound_returns404() { - Response r = globalExceptionMapper.mapRuntimeException( + Response r = globalExceptionMapper.toResponse( new jakarta.ws.rs.NotFoundException("Ressource introuvable")); assertThat(r.getStatus()).isEqualTo(404); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("error")).isEqualTo("Non trouvé"); + assertThat(body.get("error")).isEqualTo("Ressource introuvable"); } @Test @DisplayName("WebApplicationException 400 avec message non vide → 400") void mapRuntimeException_webApp4xx_withMessage_returns4xx() { - Response r = globalExceptionMapper.mapRuntimeException( + Response r = globalExceptionMapper.toResponse( new jakarta.ws.rs.WebApplicationException("Bad Request", jakarta.ws.rs.core.Response.status(400).build())); assertThat(r.getStatus()).isEqualTo(400); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("error")).isEqualTo("Erreur Client"); - assertThat(body.get("message")).isEqualTo("Bad Request"); + assertThat(body.get("error")).isEqualTo("Bad Request"); } @Test - @DisplayName("WebApplicationException 404 avec message null → Détails non disponibles") - void mapRuntimeException_webApp4xx_messageNull_returnsDetailsNonDisponibles() { - Response r = globalExceptionMapper.mapRuntimeException( + @DisplayName("WebApplicationException 404 avec message null → An error occurred") + void mapRuntimeException_webApp4xx_messageNull_returnsDefaultMessage() { + Response r = globalExceptionMapper.toResponse( new jakarta.ws.rs.WebApplicationException((String) null, jakarta.ws.rs.core.Response.status(404).build())); assertThat(r.getStatus()).isEqualTo(404); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("error")).isEqualTo("Erreur Client"); - assertThat(body.get("message")).isEqualTo("Détails non disponibles"); + assertThat(body.get("error")).isEqualTo("An error occurred"); } @Test - @DisplayName("WebApplicationException 403 avec message vide → Détails non disponibles") - void mapRuntimeException_webApp4xx_messageEmpty_returnsDetailsNonDisponibles() { - Response r = globalExceptionMapper.mapRuntimeException( + @DisplayName("WebApplicationException 403 avec message vide → message vide retourné") + void mapRuntimeException_webApp4xx_messageEmpty_returnsEmptyMessage() { + Response r = globalExceptionMapper.toResponse( new jakarta.ws.rs.WebApplicationException("", jakarta.ws.rs.core.Response.status(403).build())); assertThat(r.getStatus()).isEqualTo(403); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("message")).isEqualTo("Détails non disponibles"); + assertThat(body.get("error")).isEqualTo(""); } @Test - @DisplayName("WebApplicationException 500 → pas dans 4xx, fallback 500") - void mapRuntimeException_webApp5xx_fallbackTo500() { - Response r = globalExceptionMapper.mapRuntimeException( + @DisplayName("WebApplicationException 500 → Internal server error") + void mapRuntimeException_webApp5xx_returns500() { + Response r = globalExceptionMapper.toResponse( new jakarta.ws.rs.WebApplicationException("Server Error", jakarta.ws.rs.core.Response.status(500).build())); assertThat(r.getStatus()).isEqualTo(500); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("error")).isEqualTo("Erreur interne"); + assertThat(body.get("error")).isEqualTo("Internal server error"); } @Test - @DisplayName("WebApplicationException 399 → pas 4xx client, fallback 500") - void mapRuntimeException_webApp399_fallbackTo500() { - Response r = globalExceptionMapper.mapRuntimeException( + @DisplayName("WebApplicationException 399 → retourne le status 399 tel quel") + void mapRuntimeException_webApp399_returns399() { + Response r = globalExceptionMapper.toResponse( new jakarta.ws.rs.WebApplicationException("OK", jakarta.ws.rs.core.Response.status(399).build())); - assertThat(r.getStatus()).isEqualTo(500); - assertThat(((java.util.Map) r.getEntity()).get("error")).isEqualTo("Erreur interne"); + assertThat(r.getStatus()).isEqualTo(399); + assertThat(((java.util.Map) r.getEntity()).get("error")).isEqualTo("OK"); } @Test @DisplayName("BadRequestException → 400") void mapBadRequestException_returns400() { - Response r = globalExceptionMapper.mapBadRequestException(new BadRequestException("requête mal formée")); + Response r = globalExceptionMapper.toResponse(new BadRequestException("requête mal formée")); assertThat(r.getStatus()).isEqualTo(400); assertThat(r.getEntity()).isNotNull(); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("message")).isEqualTo("requête mal formée"); + assertThat(body.get("error")).isEqualTo("requête mal formée"); } @Test - @DisplayName("BadRequestException avec message null → buildResponse utilise error pour message") - void mapBadRequestException_nullMessage_usesErrorAsMessage() { - Response r = globalExceptionMapper.mapBadRequestException(new BadRequestException((String) null)); + @DisplayName("BadRequestException avec message null → An error occurred") + void mapBadRequestException_nullMessage_returnsDefaultMessage() { + Response r = globalExceptionMapper.toResponse(new BadRequestException((String) null)); assertThat(r.getStatus()).isEqualTo(400); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("error")).isEqualTo("Requête mal formée"); - assertThat(body.get("message")).isEqualTo("Requête mal formée"); + assertThat(body.get("error")).isEqualTo("An error occurred"); } } @Nested - @DisplayName("mapJsonException - tous les types") + @DisplayName("JSON exceptions - toResponse traite toutes comme Internal Server Error") class MapJsonException { /** Sous-classe pour appeler le constructeur protégé MismatchedInputException(JsonParser, String). */ @@ -171,67 +168,67 @@ class GlobalExceptionMapperTest { } @Test - @DisplayName("MismatchedInputException → message spécifique") + @DisplayName("MismatchedInputException → 500 (pas gérée spécifiquement)") void mapJsonException_mismatchedInput() { - Response r = globalExceptionMapper.mapJsonException(new StubMismatchedInputException()); - assertThat(r.getStatus()).isEqualTo(400); + Response r = globalExceptionMapper.toResponse(new StubMismatchedInputException()); + assertThat(r.getStatus()).isEqualTo(500); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("message")).isEqualTo("Format JSON invalide ou body manquant"); + assertThat(body.get("error")).isEqualTo("Internal server error"); } @Test - @DisplayName("InvalidFormatException → message spécifique") + @DisplayName("InvalidFormatException → 500 (pas gérée spécifiquement)") void mapJsonException_invalidFormat() { - Response r = globalExceptionMapper.mapJsonException(new StubInvalidFormatException()); - assertThat(r.getStatus()).isEqualTo(400); + Response r = globalExceptionMapper.toResponse(new StubInvalidFormatException()); + assertThat(r.getStatus()).isEqualTo(500); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("message")).isEqualTo("Format de données invalide dans le JSON"); + assertThat(body.get("error")).isEqualTo("Internal server error"); } @Test - @DisplayName("JsonMappingException → message spécifique") + @DisplayName("JsonMappingException → 500 (pas gérée spécifiquement)") void mapJsonException_jsonMapping() { - Response r = globalExceptionMapper.mapJsonException(new JsonMappingException(null, "mapping")); - assertThat(r.getStatus()).isEqualTo(400); + Response r = globalExceptionMapper.toResponse(new JsonMappingException(null, "mapping")); + assertThat(r.getStatus()).isEqualTo(500); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("message")).isEqualTo("Erreur de mapping JSON"); + assertThat(body.get("error")).isEqualTo("Internal server error"); } @Test - @DisplayName("JsonProcessingException / cas par défaut → Erreur de format JSON") + @DisplayName("JsonParseException → 500 (pas gérée spécifiquement)") void mapJsonException_jsonProcessing() { - Response r = globalExceptionMapper.mapJsonException(new JsonParseException(null, "parse error")); - assertThat(r.getStatus()).isEqualTo(400); + Response r = globalExceptionMapper.toResponse(new JsonParseException(null, "parse error")); + assertThat(r.getStatus()).isEqualTo(500); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("message")).isEqualTo("Erreur de format JSON"); + assertThat(body.get("error")).isEqualTo("Internal server error"); } } @Nested - @DisplayName("buildResponse - branches message/details null") + @DisplayName("buildErrorResponse - vérification du format de réponse") class BuildResponseBranches { @Test - @DisplayName("buildResponse(3 args) avec message null → message = error") - void buildResponse_threeArgs_messageNull() { - Response r = globalExceptionMapper.mapBadRequestException(new BadRequestException((String) null)); + @DisplayName("Toute exception contient error, status et timestamp") + void buildResponse_containsRequiredFields() { + Response r = globalExceptionMapper.toResponse(new BadRequestException((String) null)); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body.get("message")).isEqualTo(body.get("error")); + assertThat(body).containsKeys("error", "status", "timestamp"); } @Test - @DisplayName("buildResponse(4 args) avec details null → details = message ou error") - void buildResponse_fourArgs_detailsNull() { - Response r = globalExceptionMapper.mapJsonException(new JsonParseException(null, "detail")); + @DisplayName("SecurityException → 403 Forbidden") + void buildResponse_securityException_returns403() { + Response r = globalExceptionMapper.toResponse(new SecurityException("access denied")); @SuppressWarnings("unchecked") java.util.Map body = (java.util.Map) r.getEntity(); - assertThat(body).containsKey("details"); - assertThat(body.get("details")).isEqualTo("detail"); + assertThat(r.getStatus()).isEqualTo(403); + assertThat(body.get("error")).isEqualTo("access denied"); } } }