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:
dahoud
2026-04-15 20:23:04 +00:00
parent a650b372f1
commit 719d45e1fe
25 changed files with 2120 additions and 3298 deletions

View File

@@ -0,0 +1,237 @@
-- ============================================================================
-- V28 — Messagerie temps réel entre membres
--
-- Crée les tables nécessaires pour la messagerie instantanée :
-- - contact_policies : politique de communication par organisation
-- - member_blocks : blocages unilatéraux entre membres
-- - conversations : fil de discussion (directe ou canal-rôle)
-- - conversation_participants : membres d'une conversation
-- - messages : messages texte, vocal ou image
--
-- Les notes vocales sont stockées comme messages de type VOCAL avec
-- url_fichier + duree_audio. Aucune transcription en V1.
--
-- Auteur : UnionFlow Team
-- Version : 4.0
-- Date : 2026-04-13
-- ============================================================================
-- ── Politique de communication par organisation ───────────────────────────────
CREATE TABLE IF NOT EXISTS contact_policies (
id UUID NOT NULL PRIMARY KEY,
date_creation TIMESTAMP NOT NULL,
date_modification TIMESTAMP,
cree_par VARCHAR(255),
modifie_par VARCHAR(255),
version BIGINT,
actif BOOLEAN NOT NULL DEFAULT TRUE,
organisation_id UUID NOT NULL REFERENCES organisations(id),
type_politique VARCHAR(30) NOT NULL DEFAULT 'OUVERT',
autoriser_membre_vers_membre BOOLEAN NOT NULL DEFAULT TRUE,
autoriser_membre_vers_role BOOLEAN NOT NULL DEFAULT TRUE,
autoriser_notes_vocales BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT uk_contact_policy_org UNIQUE (organisation_id)
);
-- ── Blocages unilatéraux ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS member_blocks (
id UUID NOT NULL PRIMARY KEY,
date_creation TIMESTAMP NOT NULL,
date_modification TIMESTAMP,
cree_par VARCHAR(255),
modifie_par VARCHAR(255),
version BIGINT,
actif BOOLEAN NOT NULL DEFAULT TRUE,
bloqueur_id UUID NOT NULL REFERENCES utilisateurs(id),
bloque_id UUID NOT NULL REFERENCES utilisateurs(id),
organisation_id UUID NOT NULL REFERENCES organisations(id),
CONSTRAINT uk_member_block UNIQUE (bloqueur_id, bloque_id, organisation_id),
CONSTRAINT chk_block_self CHECK (bloqueur_id <> bloque_id)
);
-- ── Conversations ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS conversations (
id UUID NOT NULL PRIMARY KEY,
date_creation TIMESTAMP NOT NULL,
date_modification TIMESTAMP,
cree_par VARCHAR(255),
modifie_par VARCHAR(255),
version BIGINT,
actif BOOLEAN NOT NULL DEFAULT TRUE,
organisation_id UUID NOT NULL REFERENCES organisations(id),
-- DIRECTE | ROLE_CANAL | GROUPE
type_conversation VARCHAR(30) NOT NULL,
-- Pour ROLE_CANAL : PRESIDENT, TRESORIER, SECRETAIRE, etc.
role_cible VARCHAR(50),
-- Titre affiché (nom du rôle ou du groupe)
titre VARCHAR(200),
-- ACTIVE | ARCHIVEE
statut VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
dernier_message_at TIMESTAMP,
nombre_messages INTEGER NOT NULL DEFAULT 0
);
-- ── Participants aux conversations ────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS conversation_participants (
id UUID NOT NULL PRIMARY KEY,
date_creation TIMESTAMP NOT NULL,
date_modification TIMESTAMP,
cree_par VARCHAR(255),
modifie_par VARCHAR(255),
version BIGINT,
actif BOOLEAN NOT NULL DEFAULT TRUE,
conversation_id UUID NOT NULL REFERENCES conversations(id),
membre_id UUID NOT NULL REFERENCES utilisateurs(id),
-- INITIATEUR | PARTICIPANT | MODERATEUR
role_dans_conversation VARCHAR(50) DEFAULT 'PARTICIPANT',
-- Dernier message lu par ce participant
lu_jusqu_a TIMESTAMP,
notifier BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT uk_conv_participant UNIQUE (conversation_id, membre_id)
);
-- ── Messages ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS messages (
id UUID NOT NULL PRIMARY KEY,
date_creation TIMESTAMP NOT NULL,
date_modification TIMESTAMP,
cree_par VARCHAR(255),
modifie_par VARCHAR(255),
version BIGINT,
actif BOOLEAN NOT NULL DEFAULT TRUE,
conversation_id UUID NOT NULL REFERENCES conversations(id),
expediteur_id UUID NOT NULL REFERENCES utilisateurs(id),
-- TEXTE | VOCAL | IMAGE | SYSTEME
type_message VARCHAR(20) NOT NULL DEFAULT 'TEXTE',
-- Contenu textuel (null pour les vocaux/images)
contenu TEXT,
-- URL du fichier audio ou image (stocké sur object storage)
url_fichier VARCHAR(500),
-- Durée en secondes pour les notes vocales
duree_audio INTEGER,
-- Transcription automatique V2 (null en V1)
transcription TEXT,
-- Réponse à un message (threading léger)
message_parent_id UUID REFERENCES messages(id),
-- Suppression douce (null = non supprimé)
supprime_le TIMESTAMP
);
-- ── Ajout colonnes v4 manquantes sur tables existantes ───────────────────────
-- (les tables ont pu être créées par une migration antérieure avec le schéma v1)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversations' AND column_name = 'type_conversation') THEN
ALTER TABLE conversations ADD COLUMN type_conversation VARCHAR(30) NOT NULL DEFAULT 'DIRECTE';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversations' AND column_name = 'role_cible') THEN
ALTER TABLE conversations ADD COLUMN role_cible VARCHAR(50);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversations' AND column_name = 'titre') THEN
ALTER TABLE conversations ADD COLUMN titre VARCHAR(200);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversations' AND column_name = 'statut') THEN
ALTER TABLE conversations ADD COLUMN statut VARCHAR(20) NOT NULL DEFAULT 'ACTIVE';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversations' AND column_name = 'dernier_message_at') THEN
ALTER TABLE conversations ADD COLUMN dernier_message_at TIMESTAMP;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversations' AND column_name = 'nombre_messages') THEN
ALTER TABLE conversations ADD COLUMN nombre_messages INTEGER NOT NULL DEFAULT 0;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversations' AND column_name = 'organisation_id') THEN
ALTER TABLE conversations ADD COLUMN organisation_id UUID REFERENCES organisations(id);
END IF;
-- Messages : colonnes FK v4 (nécessaires pour les index)
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'conversation_id') THEN
ALTER TABLE messages ADD COLUMN conversation_id UUID REFERENCES conversations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'expediteur_id') THEN
ALTER TABLE messages ADD COLUMN expediteur_id UUID REFERENCES utilisateurs(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'message_parent_id') THEN
ALTER TABLE messages ADD COLUMN message_parent_id UUID REFERENCES messages(id);
END IF;
-- Messages : colonnes métier v4
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'type_message') THEN
ALTER TABLE messages ADD COLUMN type_message VARCHAR(20) NOT NULL DEFAULT 'TEXTE';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'contenu') THEN
ALTER TABLE messages ADD COLUMN contenu TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'url_fichier') THEN
ALTER TABLE messages ADD COLUMN url_fichier VARCHAR(500);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'duree_audio') THEN
ALTER TABLE messages ADD COLUMN duree_audio INTEGER;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'transcription') THEN
ALTER TABLE messages ADD COLUMN transcription TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'messages' AND column_name = 'supprime_le') THEN
ALTER TABLE messages ADD COLUMN supprime_le TIMESTAMP;
END IF;
-- ConversationParticipants : colonnes FK v4
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversation_participants' AND column_name = 'conversation_id') THEN
ALTER TABLE conversation_participants ADD COLUMN conversation_id UUID REFERENCES conversations(id);
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversation_participants' AND column_name = 'membre_id') THEN
ALTER TABLE conversation_participants ADD COLUMN membre_id UUID REFERENCES utilisateurs(id);
END IF;
-- ConversationParticipants : colonnes métier v4
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversation_participants' AND column_name = 'role_dans_conversation') THEN
ALTER TABLE conversation_participants ADD COLUMN role_dans_conversation VARCHAR(50) DEFAULT 'PARTICIPANT';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversation_participants' AND column_name = 'lu_jusqu_a') THEN
ALTER TABLE conversation_participants ADD COLUMN lu_jusqu_a TIMESTAMP;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_name = 'conversation_participants' AND column_name = 'notifier') THEN
ALTER TABLE conversation_participants ADD COLUMN notifier BOOLEAN NOT NULL DEFAULT TRUE;
END IF;
END $$;
-- ── Index de performance ──────────────────────────────────────────────────────
CREATE INDEX IF NOT EXISTS idx_conversations_organisation ON conversations(organisation_id);
CREATE INDEX IF NOT EXISTS idx_conversations_statut ON conversations(statut);
CREATE INDEX IF NOT EXISTS idx_conversations_dernier_msg ON conversations(dernier_message_at DESC NULLS LAST);
CREATE INDEX IF NOT EXISTS idx_conv_part_conversation ON conversation_participants(conversation_id);
CREATE INDEX IF NOT EXISTS idx_conv_part_membre ON conversation_participants(membre_id);
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
CREATE INDEX IF NOT EXISTS idx_messages_expediteur ON messages(expediteur_id);
CREATE INDEX IF NOT EXISTS idx_messages_date_creation ON messages(date_creation DESC);
CREATE INDEX IF NOT EXISTS idx_messages_parent ON messages(message_parent_id);
CREATE INDEX IF NOT EXISTS idx_contact_policies_org ON contact_policies(organisation_id);
CREATE INDEX IF NOT EXISTS idx_member_blocks_bloqueur ON member_blocks(bloqueur_id);
CREATE INDEX IF NOT EXISTS idx_member_blocks_bloque ON member_blocks(bloque_id, organisation_id);