feat(messaging): module messagerie unifié avec contact policies + member blocks
Refactor complet : fusion de Conversation + Message en un module Messaging unique avec ContactPolicy (règles qui-peut-parler-à-qui) et MemberBlock (blocages utilisateur). - Migration V28 : tables conversations/conversation_participants/messages/ contact_policies/member_blocks - Nouvelles entités : ContactPolicy, ConversationParticipant, MemberBlock (Conversation/Message mises à jour avec relations) - Nouvelles repositories : ContactPolicyRepository, ConversationParticipantRepository, MemberBlockRepository - MessagingResource (nouveau) remplace ConversationResource + MessageResource - MessagingService (nouveau) remplace ConversationService + MessageService avec vérifications appartenance org + policies + blocages avant envoi - Anciens fichiers Conversation/Message Resource/Service/Tests supprimés
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Politique de communication d'une organisation.
|
||||
*
|
||||
* <p>Chaque organisation possède exactement une politique, créée automatiquement
|
||||
* lors de la création de l'organisation avec les valeurs par défaut.
|
||||
* L'administrateur peut la modifier via l'API.
|
||||
*
|
||||
* <p>Table : {@code contact_policies}
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "contact_policies",
|
||||
indexes = {
|
||||
@Index(name = "idx_contact_policies_org", columnList = "organisation_id")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_contact_policy_org", columnNames = "organisation_id")
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ContactPolicy extends BaseEntity {
|
||||
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "organisation_id", nullable = false)
|
||||
private Organisation organisation;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Builder.Default
|
||||
@Column(name = "type_politique", nullable = false, length = 30)
|
||||
private TypePolitiqueCommunication typePolitique = TypePolitiqueCommunication.OUVERT;
|
||||
|
||||
@Builder.Default
|
||||
@Column(name = "autoriser_membre_vers_membre", nullable = false)
|
||||
private Boolean autoriserMembreVersMembre = Boolean.TRUE;
|
||||
|
||||
@Builder.Default
|
||||
@Column(name = "autoriser_membre_vers_role", nullable = false)
|
||||
private Boolean autoriserMembreVersRole = Boolean.TRUE;
|
||||
|
||||
@Builder.Default
|
||||
@Column(name = "autoriser_notes_vocales", nullable = false)
|
||||
private Boolean autoriserNotesVocales = Boolean.TRUE;
|
||||
|
||||
@PrePersist
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (typePolitique == null) {
|
||||
typePolitique = TypePolitiqueCommunication.OUVERT;
|
||||
}
|
||||
if (autoriserMembreVersMembre == null) {
|
||||
autoriserMembreVersMembre = Boolean.TRUE;
|
||||
}
|
||||
if (autoriserMembreVersRole == null) {
|
||||
autoriserMembreVersRole = Boolean.TRUE;
|
||||
}
|
||||
if (autoriserNotesVocales == null) {
|
||||
autoriserNotesVocales = Boolean.TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +1,129 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.communication.ConversationType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Entité Conversation pour le système de messagerie UnionFlow.
|
||||
* Représente un fil de discussion entre membres.
|
||||
* Fil de discussion entre membres d'une organisation.
|
||||
*
|
||||
* <p>Deux types sont supportés en V1 :
|
||||
* <ul>
|
||||
* <li>{@link TypeConversation#DIRECTE} — 1-1 entre deux membres</li>
|
||||
* <li>{@link TypeConversation#ROLE_CANAL} — membre vers un rôle officiel
|
||||
* (PRESIDENT, TRESORIER, SECRETAIRE…). Tous les porteurs du rôle répondent.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Table : {@code conversations}
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-16
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "conversations", indexes = {
|
||||
@Index(name = "idx_conversation_organisation", columnList = "organisation_id"),
|
||||
@Index(name = "idx_conversation_type", columnList = "type"),
|
||||
@Index(name = "idx_conversation_archived", columnList = "is_archived"),
|
||||
@Index(name = "idx_conversation_created", columnList = "date_creation")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@Table(
|
||||
name = "conversations",
|
||||
indexes = {
|
||||
@Index(name = "idx_conversations_organisation", columnList = "organisation_id"),
|
||||
@Index(name = "idx_conversations_statut", columnList = "statut"),
|
||||
@Index(name = "idx_conversations_dernier_msg", columnList = "dernier_message_at")
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Conversation extends BaseEntity {
|
||||
|
||||
/**
|
||||
* Nom de la conversation
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 255)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* Description optionnelle
|
||||
*/
|
||||
@Column(name = "description", length = 1000)
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* Type de conversation (INDIVIDUAL, GROUP, BROADCAST, ANNOUNCEMENT)
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "type", nullable = false, length = 20)
|
||||
private ConversationType type;
|
||||
|
||||
/**
|
||||
* Organisation associée (optionnelle)
|
||||
*/
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "organisation_id")
|
||||
@JoinColumn(name = "organisation_id", nullable = false)
|
||||
private Organisation organisation;
|
||||
|
||||
/**
|
||||
* URL de l'avatar de la conversation
|
||||
*/
|
||||
@Column(name = "avatar_url", length = 500)
|
||||
private String avatarUrl;
|
||||
@NotNull
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "type_conversation", nullable = false, length = 30)
|
||||
private TypeConversation typeConversation;
|
||||
|
||||
/**
|
||||
* Conversation muette
|
||||
* Rôle cible pour les ROLE_CANAL (ex : "TRESORIER", "PRESIDENT").
|
||||
* Null pour les conversations DIRECTE.
|
||||
*/
|
||||
@Column(name = "is_muted", nullable = false)
|
||||
private Boolean isMuted = false;
|
||||
@Column(name = "role_cible", length = 50)
|
||||
private String roleCible;
|
||||
|
||||
/**
|
||||
* Conversation épinglée
|
||||
*/
|
||||
@Column(name = "is_pinned", nullable = false)
|
||||
private Boolean isPinned = false;
|
||||
/** Titre affiché (nom du rôle ou du groupe, null pour DIRECTE). */
|
||||
@Column(name = "titre", length = 200)
|
||||
private String titre;
|
||||
|
||||
/**
|
||||
* Conversation archivée
|
||||
*/
|
||||
@Column(name = "is_archived", nullable = false)
|
||||
private Boolean isArchived = false;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Builder.Default
|
||||
@Column(name = "statut", nullable = false, length = 20)
|
||||
private StatutConversation statut = StatutConversation.ACTIVE;
|
||||
|
||||
/**
|
||||
* Métadonnées additionnelles (JSON)
|
||||
*/
|
||||
@Column(name = "metadata", columnDefinition = "TEXT")
|
||||
private String metadata;
|
||||
@Column(name = "dernier_message_at")
|
||||
private LocalDateTime dernierMessageAt;
|
||||
|
||||
/**
|
||||
* Date de dernière mise à jour
|
||||
*/
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
@Builder.Default
|
||||
@Column(name = "nombre_messages", nullable = false)
|
||||
private Integer nombreMessages = 0;
|
||||
|
||||
/**
|
||||
* Participants de la conversation (many-to-many)
|
||||
*/
|
||||
@ManyToMany(fetch = FetchType.LAZY)
|
||||
@JoinTable(
|
||||
name = "conversation_participants",
|
||||
joinColumns = @JoinColumn(name = "conversation_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "membre_id")
|
||||
)
|
||||
private List<Membre> participants = new ArrayList<>();
|
||||
@Builder.Default
|
||||
@OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<ConversationParticipant> participants = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Messages de la conversation (one-to-many)
|
||||
*/
|
||||
@OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
@OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<Message> messages = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Met à jour le timestamp
|
||||
*/
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
@PrePersist
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (statut == null) {
|
||||
statut = StatutConversation.ACTIVE;
|
||||
}
|
||||
if (nombreMessages == null) {
|
||||
nombreMessages = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Méthodes métier ───────────────────────────────────────────────────────
|
||||
|
||||
/** Retourne true si la conversation accepte encore de nouveaux messages. */
|
||||
public boolean estActive() {
|
||||
return StatutConversation.ACTIVE.equals(statut);
|
||||
}
|
||||
|
||||
/** Archive la conversation — plus aucun message n'est accepté. */
|
||||
public void archiver() {
|
||||
this.statut = StatutConversation.ARCHIVEE;
|
||||
}
|
||||
|
||||
/** Incrémente le compteur et met à jour l'horodatage du dernier message. */
|
||||
public void enregistrerNouveauMessage() {
|
||||
this.nombreMessages = (this.nombreMessages == null ? 0 : this.nombreMessages) + 1;
|
||||
this.dernierMessageAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Participation d'un membre à une conversation.
|
||||
*
|
||||
* <p>Stocke l'état de lecture individuel ({@code luJusqua}) et
|
||||
* les préférences de notification du participant.
|
||||
*
|
||||
* <p>Table : {@code conversation_participants}
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "conversation_participants",
|
||||
indexes = {
|
||||
@Index(name = "idx_conv_part_conversation", columnList = "conversation_id"),
|
||||
@Index(name = "idx_conv_part_membre", columnList = "membre_id")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_conv_participant",
|
||||
columnNames = {"conversation_id", "membre_id"})
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ConversationParticipant extends BaseEntity {
|
||||
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "conversation_id", nullable = false)
|
||||
private Conversation conversation;
|
||||
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "membre_id", nullable = false)
|
||||
private Membre membre;
|
||||
|
||||
/**
|
||||
* Rôle de ce participant dans la conversation.
|
||||
* Ex : INITIATEUR, PARTICIPANT, MODERATEUR.
|
||||
*/
|
||||
@Builder.Default
|
||||
@Column(name = "role_dans_conversation", length = 50)
|
||||
private String roleDansConversation = "PARTICIPANT";
|
||||
|
||||
/**
|
||||
* Horodatage du dernier message lu.
|
||||
* Permet de calculer le nombre de messages non lus.
|
||||
*/
|
||||
@Column(name = "lu_jusqu_a")
|
||||
private LocalDateTime luJusqua;
|
||||
|
||||
/** Si false, ce participant ne reçoit plus de notifications pour cette conversation. */
|
||||
@Builder.Default
|
||||
@Column(name = "notifier", nullable = false)
|
||||
private Boolean notifier = Boolean.TRUE;
|
||||
|
||||
// ── Méthodes métier ───────────────────────────────────────────────────────
|
||||
|
||||
/** Marque tous les messages jusqu'à maintenant comme lus. */
|
||||
public void marquerLu() {
|
||||
this.luJusqua = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/** Retourne true si ce participant est l'initiateur de la conversation. */
|
||||
public boolean estInitiateur() {
|
||||
return "INITIATEUR".equals(roleDansConversation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Blocage unilatéral entre deux membres au sein d'une organisation.
|
||||
*
|
||||
* <p>Un membre bloqué ne peut plus envoyer de messages au bloqueur.
|
||||
* Le blocage est limité à une organisation (un membre bloqué dans l'asso X
|
||||
* peut encore écrire dans la tontine Y).
|
||||
*
|
||||
* <p>Table : {@code member_blocks}
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "member_blocks",
|
||||
indexes = {
|
||||
@Index(name = "idx_member_blocks_bloqueur", columnList = "bloqueur_id"),
|
||||
@Index(name = "idx_member_blocks_bloque", columnList = "bloque_id, organisation_id")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_member_block",
|
||||
columnNames = {"bloqueur_id", "bloque_id", "organisation_id"})
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MemberBlock extends BaseEntity {
|
||||
|
||||
/** Membre qui effectue le blocage */
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "bloqueur_id", nullable = false)
|
||||
private Membre bloqueur;
|
||||
|
||||
/** Membre qui est bloqué */
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "bloque_id", nullable = false)
|
||||
private Membre bloque;
|
||||
|
||||
/** Organisation dans laquelle le blocage est actif */
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "organisation_id", nullable = false)
|
||||
private Organisation organisation;
|
||||
}
|
||||
@@ -1,156 +1,140 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.communication.MessagePriority;
|
||||
import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
|
||||
import dev.lions.unionflow.server.api.enums.communication.MessageType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Entité Message pour le système de messagerie UnionFlow.
|
||||
* Représente un message individuel dans une conversation.
|
||||
* Message envoyé dans une conversation.
|
||||
*
|
||||
* <p>Supporte trois types de contenu :
|
||||
* <ul>
|
||||
* <li>{@link TypeContenu#TEXTE} — message texte classique</li>
|
||||
* <li>{@link TypeContenu#VOCAL} — note vocale (Opus/AAC), stockée sur object storage.
|
||||
* Champs {@code urlFichier} + {@code dureeAudio} obligatoires.</li>
|
||||
* <li>{@link TypeContenu#IMAGE} — image JPEG/PNG. Champ {@code urlFichier} obligatoire.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>La suppression est douce : {@code supprimeLe} est renseigné au lieu de
|
||||
* supprimer la ligne. Le contenu devient {@code "[Message supprimé]"}.
|
||||
*
|
||||
* <p>Table : {@code messages}
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-16
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "messages", indexes = {
|
||||
@Index(name = "idx_message_conversation", columnList = "conversation_id"),
|
||||
@Index(name = "idx_message_sender", columnList = "sender_id"),
|
||||
@Index(name = "idx_message_organisation", columnList = "organisation_id"),
|
||||
@Index(name = "idx_message_status", columnList = "status"),
|
||||
@Index(name = "idx_message_created", columnList = "date_creation"),
|
||||
@Index(name = "idx_message_deleted", columnList = "is_deleted")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@Table(
|
||||
name = "messages",
|
||||
indexes = {
|
||||
@Index(name = "idx_messages_conversation", columnList = "conversation_id"),
|
||||
@Index(name = "idx_messages_expediteur", columnList = "expediteur_id"),
|
||||
@Index(name = "idx_messages_date_creation", columnList = "date_creation"),
|
||||
@Index(name = "idx_messages_parent", columnList = "message_parent_id")
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Message extends BaseEntity {
|
||||
|
||||
/**
|
||||
* Conversation parente
|
||||
*/
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "conversation_id", nullable = false)
|
||||
private Conversation conversation;
|
||||
|
||||
/**
|
||||
* Expéditeur du message
|
||||
*/
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "sender_id", nullable = false)
|
||||
private Membre sender;
|
||||
@JoinColumn(name = "expediteur_id", nullable = false)
|
||||
private Membre expediteur;
|
||||
|
||||
/**
|
||||
* Nom de l'expéditeur (dénormalisé pour performance)
|
||||
*/
|
||||
@Column(name = "sender_name", nullable = false, length = 255)
|
||||
private String senderName;
|
||||
|
||||
/**
|
||||
* Avatar de l'expéditeur (dénormalisé)
|
||||
*/
|
||||
@Column(name = "sender_avatar", length = 500)
|
||||
private String senderAvatar;
|
||||
|
||||
/**
|
||||
* Contenu du message
|
||||
*/
|
||||
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* Type de message (INDIVIDUAL, BROADCAST, TARGETED, SYSTEM)
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "type", nullable = false, length = 20)
|
||||
private MessageType type;
|
||||
@Builder.Default
|
||||
@Column(name = "type_message", nullable = false, length = 20)
|
||||
private TypeContenu typeMessage = TypeContenu.TEXTE;
|
||||
|
||||
/** Texte du message — null pour les vocaux/images. */
|
||||
@Column(name = "contenu", columnDefinition = "TEXT")
|
||||
private String contenu;
|
||||
|
||||
/**
|
||||
* Statut du message (SENT, DELIVERED, READ, FAILED)
|
||||
* URL du fichier audio (notes vocales) ou image.
|
||||
* Format : https://storage.lions.dev/chat/{conversationId}/{messageId}.opus
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private MessageStatus status;
|
||||
@Column(name = "url_fichier", length = 500)
|
||||
private String urlFichier;
|
||||
|
||||
/** Durée en secondes pour les notes vocales. */
|
||||
@Column(name = "duree_audio")
|
||||
private Integer dureeAudio;
|
||||
|
||||
/**
|
||||
* Priorité du message (NORMAL, HIGH, URGENT)
|
||||
* Transcription automatique du vocal — null en V1.
|
||||
* Sera renseigné par un service Speech-to-Text en V2.
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "priority", nullable = false, length = 20)
|
||||
private MessagePriority priority = MessagePriority.NORMAL;
|
||||
@Column(name = "transcription", columnDefinition = "TEXT")
|
||||
private String transcription;
|
||||
|
||||
/**
|
||||
* IDs des destinataires (CSV pour targeted messages)
|
||||
*/
|
||||
@Column(name = "recipient_ids", length = 2000)
|
||||
private String recipientIds;
|
||||
|
||||
/**
|
||||
* Rôles destinataires (CSV pour role-based messaging)
|
||||
*/
|
||||
@Column(name = "recipient_roles", length = 500)
|
||||
private String recipientRoles;
|
||||
|
||||
/**
|
||||
* Organisation associée (optionnelle)
|
||||
*/
|
||||
/** Message auquel celui-ci répond (threading léger). */
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "organisation_id")
|
||||
private Organisation organisation;
|
||||
@JoinColumn(name = "message_parent_id")
|
||||
private Message messageParent;
|
||||
|
||||
/**
|
||||
* Date de lecture du message
|
||||
*/
|
||||
@Column(name = "read_at")
|
||||
private LocalDateTime readAt;
|
||||
/** Date de suppression douce (null = message actif). */
|
||||
@Column(name = "supprime_le")
|
||||
private LocalDateTime supprimeLe;
|
||||
|
||||
/**
|
||||
* Métadonnées additionnelles (JSON)
|
||||
*/
|
||||
@Column(name = "metadata", columnDefinition = "TEXT")
|
||||
private String metadata;
|
||||
@PrePersist
|
||||
@Override
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (typeMessage == null) {
|
||||
typeMessage = TypeContenu.TEXTE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pièces jointes (CSV URLs)
|
||||
*/
|
||||
@Column(name = "attachments", length = 2000)
|
||||
private String attachments;
|
||||
// ── Méthodes métier ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Message édité
|
||||
*/
|
||||
@Column(name = "is_edited", nullable = false)
|
||||
private Boolean isEdited = false;
|
||||
/** Retourne true si le message a été supprimé par son auteur. */
|
||||
public boolean estSupprime() {
|
||||
return supprimeLe != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Date d'édition
|
||||
*/
|
||||
@Column(name = "edited_at")
|
||||
private LocalDateTime editedAt;
|
||||
/** Retourne true si c'est un message texte. */
|
||||
public boolean estTextuel() {
|
||||
return TypeContenu.TEXTE.equals(typeMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Message supprimé (soft delete)
|
||||
*/
|
||||
@Column(name = "is_deleted", nullable = false)
|
||||
private Boolean isDeleted = false;
|
||||
|
||||
/**
|
||||
* Marque le message comme lu
|
||||
*/
|
||||
public void markAsRead() {
|
||||
this.status = MessageStatus.READ;
|
||||
this.readAt = LocalDateTime.now();
|
||||
/** Retourne true si c'est une note vocale. */
|
||||
public boolean estVocal() {
|
||||
return TypeContenu.VOCAL.equals(typeMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque le message comme édité
|
||||
* Supprime le message de façon douce.
|
||||
* Le contenu original est remplacé par un marqueur.
|
||||
*/
|
||||
public void markAsEdited() {
|
||||
this.isEdited = true;
|
||||
this.editedAt = LocalDateTime.now();
|
||||
public void supprimer() {
|
||||
this.supprimeLe = LocalDateTime.now();
|
||||
this.contenu = "[Message supprimé]";
|
||||
this.urlFichier = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user