Compare commits
10 Commits
aebf333421
...
4e1a6d4007
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e1a6d4007 | ||
|
|
2f7bb545d0 | ||
|
|
66151b4fd1 | ||
|
|
6ff85bd503 | ||
|
|
e482ad5a4d | ||
|
|
9a270995ee | ||
|
|
217021933e | ||
|
|
5d028a10bf | ||
|
|
719d45e1fe | ||
|
|
a650b372f1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -120,3 +120,5 @@ uploads/
|
||||
|
||||
# Claude Code agent worktrees
|
||||
.claude/
|
||||
du.exe.stackdump
|
||||
du.exe.stackdump
|
||||
|
||||
4
pom.xml
4
pom.xml
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>dev.lions.unionflow</groupId>
|
||||
<artifactId>unionflow-parent</artifactId>
|
||||
<version>1.0.4</version>
|
||||
<version>1.0.5</version>
|
||||
<relativePath>../unionflow-server-api/parent-pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<dependency>
|
||||
<groupId>dev.lions.unionflow</groupId>
|
||||
<artifactId>unionflow-server-api</artifactId>
|
||||
<version>1.0.4</version>
|
||||
<version>1.0.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Lions User Manager API (pour DTOs et client Keycloak) -->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -15,8 +14,8 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Entité Paiement centralisée pour tous les types de paiements
|
||||
* Réutilisable pour cotisations, adhésions, événements, aides
|
||||
* Entité Paiement centralisée pour tous les types de paiements.
|
||||
* Réutilisable pour cotisations, adhésions, événements, aides.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
@@ -104,7 +103,7 @@ public class Paiement extends BaseEntity {
|
||||
@JoinColumn(name = "membre_id", nullable = false)
|
||||
private Membre membre;
|
||||
|
||||
/** Objets cibles de ce paiement (Cat.2 — polymorphique) */
|
||||
/** Objets cibles de ce paiement (polymorphique) */
|
||||
@JsonIgnore
|
||||
@OneToMany(mappedBy = "paiement", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
@Builder.Default
|
||||
@@ -115,18 +114,15 @@ public class Paiement extends BaseEntity {
|
||||
@JoinColumn(name = "transaction_wave_id")
|
||||
private TransactionWave transactionWave;
|
||||
|
||||
private static final AtomicLong REFERENCE_COUNTER =
|
||||
new AtomicLong(System.currentTimeMillis() % 1000000000000L);
|
||||
|
||||
/** Méthode métier pour générer un numéro de référence unique */
|
||||
/** Génère un numéro de référence unique */
|
||||
public static String genererNumeroReference() {
|
||||
return "PAY-"
|
||||
+ LocalDateTime.now().getYear()
|
||||
+ "-"
|
||||
+ String.format("%012d", REFERENCE_COUNTER.incrementAndGet() % 1000000000000L);
|
||||
+ String.format("%012d", System.currentTimeMillis() % 1000000000000L);
|
||||
}
|
||||
|
||||
/** Méthode métier pour vérifier si le paiement est validé */
|
||||
/** Vérifie si le paiement est validé */
|
||||
public boolean isValide() {
|
||||
return "VALIDE".equals(statutPaiement);
|
||||
}
|
||||
@@ -137,12 +133,10 @@ public class Paiement extends BaseEntity {
|
||||
&& !"ANNULE".equals(statutPaiement);
|
||||
}
|
||||
|
||||
/** Callback JPA avant la persistance */
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (numeroReference == null
|
||||
|| numeroReference.isEmpty()) {
|
||||
if (numeroReference == null || numeroReference.isEmpty()) {
|
||||
numeroReference = genererNumeroReference();
|
||||
}
|
||||
if (statutPaiement == null) {
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
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.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Digits;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
@@ -24,23 +12,11 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Table de liaison polymorphique entre un paiement
|
||||
* et son objet cible.
|
||||
* Table de liaison polymorphique entre un paiement et son objet cible.
|
||||
*
|
||||
* <p>
|
||||
* Remplace les 4 tables dupliquées
|
||||
* {@code paiements_cotisations},
|
||||
* {@code paiements_adhesions},
|
||||
* {@code paiements_evenements} et
|
||||
* {@code paiements_aides} par une table unique
|
||||
* utilisant le pattern
|
||||
* {@code (type_objet_cible, objet_cible_id)}.
|
||||
*
|
||||
* <p>
|
||||
* Les types d'objet cible sont définis dans le
|
||||
* domaine {@code OBJET_PAIEMENT} de la table
|
||||
* {@code types_reference} (ex: COTISATION,
|
||||
* ADHESION, EVENEMENT, AIDE).
|
||||
* <p>Remplace les tables dupliquées {@code paiements_cotisations},
|
||||
* {@code paiements_adhesions}, etc. par une table unique utilisant
|
||||
* le pattern {@code (type_objet_cible, objet_cible_id)}.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
@@ -48,16 +24,12 @@ import lombok.NoArgsConstructor;
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "paiements_objets", indexes = {
|
||||
@Index(name = "idx_po_paiement", columnList = "paiement_id"),
|
||||
@Index(name = "idx_po_objet", columnList = "type_objet_cible,"
|
||||
+ " objet_cible_id"),
|
||||
@Index(name = "idx_po_type", columnList = "type_objet_cible")
|
||||
@Index(name = "idx_po_paiement", columnList = "paiement_id"),
|
||||
@Index(name = "idx_po_objet", columnList = "type_objet_cible, objet_cible_id"),
|
||||
@Index(name = "idx_po_type", columnList = "type_objet_cible")
|
||||
}, uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_paiement_objet", columnNames = {
|
||||
"paiement_id",
|
||||
"type_objet_cible",
|
||||
"objet_cible_id"
|
||||
})
|
||||
@UniqueConstraint(name = "uk_paiement_objet",
|
||||
columnNames = {"paiement_id", "type_objet_cible", "objet_cible_id"})
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@@ -66,65 +38,47 @@ import lombok.NoArgsConstructor;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class PaiementObjet extends BaseEntity {
|
||||
|
||||
/** Paiement parent. */
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "paiement_id", nullable = false)
|
||||
private Paiement paiement;
|
||||
/** Paiement parent. */
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "paiement_id", nullable = false)
|
||||
private Paiement paiement;
|
||||
|
||||
/**
|
||||
* Type de l'objet cible (code du domaine
|
||||
* {@code OBJET_PAIEMENT} dans
|
||||
* {@code types_reference}).
|
||||
*
|
||||
* <p>
|
||||
* Valeurs attendues : {@code COTISATION},
|
||||
* {@code ADHESION}, {@code EVENEMENT},
|
||||
* {@code AIDE}.
|
||||
*/
|
||||
@NotBlank
|
||||
@Size(max = 50)
|
||||
@Column(name = "type_objet_cible", nullable = false, length = 50)
|
||||
private String typeObjetCible;
|
||||
/**
|
||||
* Type de l'objet cible (ex: COTISATION, ADHESION, EVENEMENT, AIDE).
|
||||
*/
|
||||
@NotBlank
|
||||
@Size(max = 50)
|
||||
@Column(name = "type_objet_cible", nullable = false, length = 50)
|
||||
private String typeObjetCible;
|
||||
|
||||
/**
|
||||
* UUID de l'objet cible (cotisation, demande
|
||||
* d'adhésion, inscription événement, ou demande
|
||||
* d'aide).
|
||||
*/
|
||||
@NotNull
|
||||
@Column(name = "objet_cible_id", nullable = false)
|
||||
private UUID objetCibleId;
|
||||
/** UUID de l'objet cible. */
|
||||
@NotNull
|
||||
@Column(name = "objet_cible_id", nullable = false)
|
||||
private UUID objetCibleId;
|
||||
|
||||
/** Montant appliqué à cet objet cible. */
|
||||
@NotNull
|
||||
@DecimalMin(value = "0.0", message = "Le montant doit être positif")
|
||||
@Digits(integer = 12, fraction = 2)
|
||||
@Column(name = "montant_applique", nullable = false, precision = 14, scale = 2)
|
||||
private BigDecimal montantApplique;
|
||||
/** Montant appliqué à cet objet cible. */
|
||||
@NotNull
|
||||
@DecimalMin(value = "0.0", message = "Le montant doit être positif")
|
||||
@Digits(integer = 12, fraction = 2)
|
||||
@Column(name = "montant_applique", nullable = false, precision = 14, scale = 2)
|
||||
private BigDecimal montantApplique;
|
||||
|
||||
/** Date d'application du paiement. */
|
||||
@Column(name = "date_application")
|
||||
private LocalDateTime dateApplication;
|
||||
/** Date d'application du paiement. */
|
||||
@Column(name = "date_application")
|
||||
private LocalDateTime dateApplication;
|
||||
|
||||
/** Commentaire sur l'application. */
|
||||
@Size(max = 500)
|
||||
@Column(name = "commentaire", length = 500)
|
||||
private String commentaire;
|
||||
/** Commentaire sur l'application. */
|
||||
@Size(max = 500)
|
||||
@Column(name = "commentaire", length = 500)
|
||||
private String commentaire;
|
||||
|
||||
/**
|
||||
* Callback JPA avant la persistance.
|
||||
*
|
||||
* <p>
|
||||
* Initialise {@code dateApplication} si non
|
||||
* renseignée.
|
||||
*/
|
||||
@Override
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (dateApplication == null) {
|
||||
dateApplication = LocalDateTime.now();
|
||||
}
|
||||
@Override
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (dateApplication == null) {
|
||||
dateApplication = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "system_config")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SystemConfigPersistence extends BaseEntity {
|
||||
|
||||
@Column(name = "config_key", unique = true, nullable = false, length = 100)
|
||||
private String configKey;
|
||||
|
||||
@Column(name = "config_value", columnDefinition = "TEXT")
|
||||
private String configValue;
|
||||
}
|
||||
171
src/main/java/dev/lions/unionflow/server/entity/Versement.java
Normal file
171
src/main/java/dev/lions/unionflow/server/entity/Versement.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* Versement — acte de régler une cotisation ou de déposer des fonds.
|
||||
*
|
||||
* <p>Un versement peut être effectué :
|
||||
* <ul>
|
||||
* <li>Via <b>Wave Mobile Money</b> : deep link natif, app Wave sur le même téléphone</li>
|
||||
* <li>Manuellement : espèces, virement, chèque → statut EN_ATTENTE_VALIDATION</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Table DB : {@code paiements} (nom hérité, conservé pour compatibilité Flyway).
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "paiements", indexes = {
|
||||
@Index(name = "idx_paiement_numero_reference", columnList = "numero_reference", unique = true),
|
||||
@Index(name = "idx_paiement_membre", columnList = "membre_id"),
|
||||
@Index(name = "idx_paiement_statut", columnList = "statut_paiement"),
|
||||
@Index(name = "idx_paiement_methode", columnList = "methode_paiement"),
|
||||
@Index(name = "idx_paiement_date", columnList = "date_paiement")
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Versement extends BaseEntity {
|
||||
|
||||
private static final AtomicLong REFERENCE_COUNTER =
|
||||
new AtomicLong(System.currentTimeMillis() % 1_000_000_000_000L);
|
||||
|
||||
// ── Identité ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** Référence unique (ex: VRS-2026-XXXXXXXXXXXX) */
|
||||
@NotBlank
|
||||
@Column(name = "numero_reference", unique = true, nullable = false, length = 50)
|
||||
private String numeroReference;
|
||||
|
||||
// ── Montant ───────────────────────────────────────────────────────────────
|
||||
|
||||
@NotNull
|
||||
@DecimalMin(value = "0.0", message = "Le montant doit être positif")
|
||||
@Digits(integer = 12, fraction = 2)
|
||||
@Column(name = "montant", nullable = false, precision = 14, scale = 2)
|
||||
private BigDecimal montant;
|
||||
|
||||
@NotBlank
|
||||
@Pattern(regexp = "^[A-Z]{3}$", message = "Code devise ISO à 3 lettres requis")
|
||||
@Column(name = "code_devise", nullable = false, length = 3)
|
||||
private String codeDevise;
|
||||
|
||||
// ── Méthode & Statut ──────────────────────────────────────────────────────
|
||||
|
||||
/** WAVE | ESPECES | VIREMENT | CHEQUE | AUTRE */
|
||||
@NotNull
|
||||
@Column(name = "methode_paiement", nullable = false, length = 50)
|
||||
private String methodePaiement;
|
||||
|
||||
/** EN_ATTENTE | EN_COURS | CONFIRME | ECHEC | EN_ATTENTE_VALIDATION | ANNULE */
|
||||
@NotNull
|
||||
@Builder.Default
|
||||
@Column(name = "statut_paiement", nullable = false, length = 30)
|
||||
private String statutPaiement = "EN_ATTENTE";
|
||||
|
||||
// ── Dates ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@Column(name = "date_paiement")
|
||||
private LocalDateTime datePaiement;
|
||||
|
||||
@Column(name = "date_validation")
|
||||
private LocalDateTime dateValidation;
|
||||
|
||||
// ── Validation ────────────────────────────────────────────────────────────
|
||||
|
||||
@Column(name = "validateur", length = 255)
|
||||
private String validateur;
|
||||
|
||||
// ── Traçabilité ───────────────────────────────────────────────────────────
|
||||
|
||||
/** ID transaction Wave (TCN...) ou référence chèque / bordereau */
|
||||
@Column(name = "reference_externe", length = 500)
|
||||
private String referenceExterne;
|
||||
|
||||
@Column(name = "url_preuve", length = 1000)
|
||||
private String urlPreuve;
|
||||
|
||||
@Column(name = "commentaire", length = 1000)
|
||||
private String commentaire;
|
||||
|
||||
@Column(name = "ip_address", length = 45)
|
||||
private String ipAddress;
|
||||
|
||||
@Column(name = "user_agent", length = 500)
|
||||
private String userAgent;
|
||||
|
||||
// ── Téléphone Wave ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Numéro de téléphone Wave utilisé pour ce versement.
|
||||
* Pré-rempli depuis le profil du membre (même téléphone qu'UnionFlow),
|
||||
* modifiable à l'étape "Récapitulatif" avant de tapper "Payer".
|
||||
*/
|
||||
@Column(name = "numero_telephone", length = 20)
|
||||
private String numeroTelephone;
|
||||
|
||||
// ── Relations ─────────────────────────────────────────────────────────────
|
||||
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "membre_id", nullable = false)
|
||||
private Membre membre;
|
||||
|
||||
@JsonIgnore
|
||||
@OneToMany(mappedBy = "versement", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
@Builder.Default
|
||||
private List<VersementObjet> versementsObjets = new ArrayList<>();
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "transaction_wave_id")
|
||||
private TransactionWave transactionWave;
|
||||
|
||||
// ── Méthodes métier ───────────────────────────────────────────────────────
|
||||
|
||||
/** Génère une référence unique : VRS-YYYY-XXXXXXXXXXXX */
|
||||
public static String genererNumeroReference() {
|
||||
return "VRS-"
|
||||
+ LocalDateTime.now().getYear()
|
||||
+ "-"
|
||||
+ String.format("%012d", REFERENCE_COUNTER.incrementAndGet() % 1_000_000_000_000L);
|
||||
}
|
||||
|
||||
/** Vrai si le versement est confirmé (Wave) ou validé (manuel) */
|
||||
public boolean isConfirme() {
|
||||
return "CONFIRME".equals(statutPaiement) || "VALIDE".equals(statutPaiement);
|
||||
}
|
||||
|
||||
/** Vrai si le versement peut encore être modifié ou annulé */
|
||||
public boolean peutEtreModifie() {
|
||||
return !"CONFIRME".equals(statutPaiement)
|
||||
&& !"VALIDE".equals(statutPaiement)
|
||||
&& !"ANNULE".equals(statutPaiement);
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (numeroReference == null || numeroReference.isBlank()) {
|
||||
numeroReference = genererNumeroReference();
|
||||
}
|
||||
if (statutPaiement == null) {
|
||||
statutPaiement = "EN_ATTENTE";
|
||||
}
|
||||
if (datePaiement == null) {
|
||||
datePaiement = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* Liaison polymorphique entre un versement et son objet cible.
|
||||
*
|
||||
* <p>Remplace les 4 tables dupliquées (paiements_cotisations, paiements_adhesions,
|
||||
* paiements_evenements, paiements_aides) par une table unique utilisant le pattern
|
||||
* {@code (typeObjetCible, objetCibleId)}.
|
||||
*
|
||||
* <p>Table DB : {@code paiements_objets} (nom hérité, conservé pour compatibilité Flyway).
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "paiements_objets", indexes = {
|
||||
@Index(name = "idx_po_paiement", columnList = "paiement_id"),
|
||||
@Index(name = "idx_po_objet", columnList = "type_objet_cible, objet_cible_id"),
|
||||
@Index(name = "idx_po_type", columnList = "type_objet_cible")
|
||||
}, uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_paiement_objet", columnNames = {
|
||||
"paiement_id", "type_objet_cible", "objet_cible_id"
|
||||
})
|
||||
})
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class VersementObjet extends BaseEntity {
|
||||
|
||||
/** Versement parent. */
|
||||
@NotNull
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "paiement_id", nullable = false)
|
||||
private Versement versement;
|
||||
|
||||
/**
|
||||
* Type de l'objet cible (domaine {@code OBJET_PAIEMENT}).
|
||||
* Valeurs : COTISATION | ADHESION | EVENEMENT | AIDE.
|
||||
*/
|
||||
@NotBlank
|
||||
@Size(max = 50)
|
||||
@Column(name = "type_objet_cible", nullable = false, length = 50)
|
||||
private String typeObjetCible;
|
||||
|
||||
/** UUID de l'objet cible (cotisation, adhésion, inscription, aide). */
|
||||
@NotNull
|
||||
@Column(name = "objet_cible_id", nullable = false)
|
||||
private UUID objetCibleId;
|
||||
|
||||
/** Montant affecté à cet objet cible. */
|
||||
@NotNull
|
||||
@DecimalMin(value = "0.0", message = "Le montant doit être positif")
|
||||
@Digits(integer = 12, fraction = 2)
|
||||
@Column(name = "montant_applique", nullable = false, precision = 14, scale = 2)
|
||||
private BigDecimal montantApplique;
|
||||
|
||||
@Column(name = "date_application")
|
||||
private LocalDateTime dateApplication;
|
||||
|
||||
@Size(max = 500)
|
||||
@Column(name = "commentaire", length = 500)
|
||||
private String commentaire;
|
||||
|
||||
@Override
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
super.onCreate();
|
||||
if (dateApplication == null) {
|
||||
dateApplication = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ public class WebhookWave extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "paiement_id")
|
||||
private Paiement paiement;
|
||||
private Versement versement;
|
||||
|
||||
/** Méthode métier pour vérifier si le webhook est traité */
|
||||
public boolean isTraite() {
|
||||
|
||||
@@ -86,4 +86,18 @@ public class KafkaEventConsumer {
|
||||
LOG.errorf(e, "Failed to broadcast contribution event");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consomme les messages de chat (nouveaux messages envoyés dans une conversation).
|
||||
* Broadcaste l'event en temps réel aux clients WebSocket pour mise à jour instantanée.
|
||||
*/
|
||||
@Incoming("chat-messages-in")
|
||||
public void consumeChatMessages(Record<String, String> record) {
|
||||
LOG.debugf("Received chat message event: key=%s", record.key());
|
||||
try {
|
||||
webSocketBroadcastService.broadcast(record.value());
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Failed to broadcast chat message event");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.BackupRecord;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository pour les enregistrements de sauvegarde.
|
||||
* Étend BaseRepository pour cohérence avec le reste du projet.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class BackupRecordRepository implements PanacheRepositoryBase<BackupRecord, UUID> {
|
||||
public class BackupRecordRepository extends BaseRepository<BackupRecord> {
|
||||
|
||||
public BackupRecordRepository() {
|
||||
super(BackupRecord.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les enregistrements de sauvegarde triés par date décroissante.
|
||||
*/
|
||||
public List<BackupRecord> findAllOrderedByDate() {
|
||||
return findAll(Sort.by("dateCreation", Sort.Direction.Descending)).list();
|
||||
}
|
||||
|
||||
public void updateStatus(UUID id, String status, Long sizeBytes, LocalDateTime completedAt, String errorMessage) {
|
||||
/**
|
||||
* Met à jour le statut d'un enregistrement de sauvegarde.
|
||||
* Opération transactionnelle — utilisée pour passer de IN_PROGRESS à COMPLETED ou FAILED.
|
||||
*/
|
||||
@Transactional
|
||||
public void updateStatus(UUID id, String status, Long sizeBytes,
|
||||
LocalDateTime completedAt, String errorMessage) {
|
||||
update("status = ?1, sizeBytes = ?2, completedAt = ?3, errorMessage = ?4 WHERE id = ?5",
|
||||
status, sizeBytes, completedAt, errorMessage, id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.ContactPolicy;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository pour les politiques de communication des organisations.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class ContactPolicyRepository implements PanacheRepositoryBase<ContactPolicy, UUID> {
|
||||
|
||||
/**
|
||||
* Trouve la politique de communication d'une organisation.
|
||||
* Chaque organisation a exactement une politique.
|
||||
*/
|
||||
public Optional<ContactPolicy> findByOrganisationId(UUID organisationId) {
|
||||
return find("organisation.id = ?1 AND actif = true", organisationId)
|
||||
.firstResultOptional();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.ConversationParticipant;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository pour les participants aux conversations.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class ConversationParticipantRepository
|
||||
implements PanacheRepositoryBase<ConversationParticipant, UUID> {
|
||||
|
||||
/**
|
||||
* Trouve la participation d'un membre à une conversation.
|
||||
*/
|
||||
public Optional<ConversationParticipant> findParticipant(UUID conversationId, UUID membreId) {
|
||||
return find("conversation.id = ?1 AND membre.id = ?2 AND actif = true",
|
||||
conversationId, membreId
|
||||
).firstResultOptional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les participants actifs d'une conversation.
|
||||
*/
|
||||
public List<ConversationParticipant> findByConversation(UUID conversationId) {
|
||||
return find("conversation.id = ?1 AND actif = true", conversationId).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un membre est participant à une conversation.
|
||||
*/
|
||||
public boolean estParticipant(UUID conversationId, UUID membreId) {
|
||||
return count("conversation.id = ?1 AND membre.id = ?2 AND actif = true",
|
||||
conversationId, membreId) > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,80 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Conversation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository pour Conversation
|
||||
* Repository pour les conversations de la messagerie.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-16
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class ConversationRepository implements PanacheRepositoryBase<Conversation, UUID> {
|
||||
|
||||
/**
|
||||
* Trouve toutes les conversations d'un membre
|
||||
* Trouve une conversation par son ID avec Optional.
|
||||
*/
|
||||
public List<Conversation> findByParticipant(UUID membreId, boolean includeArchived) {
|
||||
String query = """
|
||||
SELECT DISTINCT c FROM Conversation c
|
||||
JOIN c.participants p
|
||||
WHERE p.id = :membreId
|
||||
AND (c.actif IS NULL OR c.actif = true)
|
||||
""";
|
||||
|
||||
if (!includeArchived) {
|
||||
query += " AND c.isArchived = false";
|
||||
}
|
||||
|
||||
query += " ORDER BY c.updatedAt DESC NULLS LAST, c.dateCreation DESC";
|
||||
|
||||
return getEntityManager()
|
||||
.createQuery(query, Conversation.class)
|
||||
.setParameter("membreId", membreId)
|
||||
.getResultList();
|
||||
public Optional<Conversation> findConversationById(UUID id) {
|
||||
return find("id", id).firstResultOptional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve une conversation par ID et vérifie que le membre en fait partie
|
||||
* Liste toutes les conversations d'un membre (via les participants).
|
||||
* Triées par date du dernier message décroissante.
|
||||
*/
|
||||
public Optional<Conversation> findByIdAndParticipant(UUID conversationId, UUID membreId) {
|
||||
String query = """
|
||||
SELECT DISTINCT c FROM Conversation c
|
||||
JOIN c.participants p
|
||||
WHERE c.id = :conversationId
|
||||
AND p.id = :membreId
|
||||
AND (c.actif IS NULL OR c.actif = true)
|
||||
""";
|
||||
|
||||
return getEntityManager()
|
||||
.createQuery(query, Conversation.class)
|
||||
.setParameter("conversationId", conversationId)
|
||||
.setParameter("membreId", membreId)
|
||||
.getResultStream()
|
||||
.findFirst();
|
||||
public List<Conversation> findByMembreId(UUID membreId) {
|
||||
return find(
|
||||
"SELECT DISTINCT c FROM Conversation c " +
|
||||
"JOIN c.participants p " +
|
||||
"WHERE p.membre.id = ?1 AND p.actif = true " +
|
||||
"ORDER BY c.dernierMessageAt DESC NULLS LAST",
|
||||
membreId
|
||||
).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve les conversations d'une organisation
|
||||
* Trouve une conversation directe existante entre deux membres dans une organisation.
|
||||
*/
|
||||
public List<Conversation> findByOrganisation(UUID organisationId) {
|
||||
return find("organisation.id = ?1 AND (actif IS NULL OR actif = true) ORDER BY updatedAt DESC NULLS LAST", organisationId)
|
||||
.list();
|
||||
public Optional<Conversation> findConversationDirecte(UUID membreAId, UUID membreBId, UUID organisationId) {
|
||||
return find(
|
||||
"SELECT DISTINCT c FROM Conversation c " +
|
||||
"JOIN c.participants p1 " +
|
||||
"JOIN c.participants p2 " +
|
||||
"WHERE c.typeConversation = 'DIRECTE' " +
|
||||
"AND c.organisation.id = ?3 " +
|
||||
"AND p1.membre.id = ?1 AND p1.actif = true " +
|
||||
"AND p2.membre.id = ?2 AND p2.actif = true",
|
||||
membreAId, membreBId, organisationId
|
||||
).firstResultOptional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve le canal d'un rôle dans une organisation.
|
||||
*/
|
||||
public Optional<Conversation> findCanalRole(UUID organisationId, String roleCible) {
|
||||
return find(
|
||||
"organisation.id = ?1 AND roleCible = ?2 AND typeConversation = 'ROLE_CANAL' AND actif = true",
|
||||
organisationId, roleCible
|
||||
).firstResultOptional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les conversations actives d'un membre.
|
||||
*/
|
||||
public List<Conversation> findActivesByMembre(UUID membreId) {
|
||||
return find(
|
||||
"SELECT DISTINCT c FROM Conversation c " +
|
||||
"JOIN c.participants p " +
|
||||
"WHERE p.membre.id = ?1 AND p.actif = true AND c.statut = ?2 " +
|
||||
"ORDER BY c.dernierMessageAt DESC NULLS LAST",
|
||||
membreId, StatutConversation.ACTIVE
|
||||
).list();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.MemberBlock;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository pour les blocages entre membres.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class MemberBlockRepository implements PanacheRepositoryBase<MemberBlock, UUID> {
|
||||
|
||||
/**
|
||||
* Vérifie si un membre en a bloqué un autre dans une organisation.
|
||||
*/
|
||||
public boolean estBloque(UUID bloqueurId, UUID bloqueId, UUID organisationId) {
|
||||
return count("bloqueur.id = ?1 AND bloque.id = ?2 AND organisation.id = ?3 AND actif = true",
|
||||
bloqueurId, bloqueId, organisationId) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve le blocage entre deux membres dans une organisation.
|
||||
*/
|
||||
public Optional<MemberBlock> findBlocage(UUID bloqueurId, UUID bloqueId, UUID organisationId) {
|
||||
return find("bloqueur.id = ?1 AND bloque.id = ?2 AND organisation.id = ?3 AND actif = true",
|
||||
bloqueurId, bloqueId, organisationId
|
||||
).firstResultOptional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les membres bloqués par un membre dans toutes ses organisations.
|
||||
*/
|
||||
public List<MemberBlock> findByBloqueur(UUID bloqueurId) {
|
||||
return find("bloqueur.id = ?1 AND actif = true", bloqueurId).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les blocages actifs d'un membre dans une organisation spécifique.
|
||||
*/
|
||||
public List<MemberBlock> findByBloqueurEtOrganisation(UUID bloqueurId, UUID organisationId) {
|
||||
return find("bloqueur.id = ?1 AND organisation.id = ?2 AND actif = true",
|
||||
bloqueurId, organisationId).list();
|
||||
}
|
||||
}
|
||||
@@ -2,65 +2,77 @@ package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.Message;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import io.quarkus.panache.common.Page;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository pour Message
|
||||
* Repository pour les messages de la messagerie.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-16
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class MessageRepository implements PanacheRepositoryBase<Message, UUID> {
|
||||
|
||||
/**
|
||||
* Trouve tous les messages d'une conversation (non supprimés)
|
||||
* Trouve un message par son ID avec Optional.
|
||||
*/
|
||||
public List<Message> findByConversation(UUID conversationId, int limit) {
|
||||
return find(
|
||||
"conversation.id = ?1 AND isDeleted = false AND (actif IS NULL OR actif = true) ORDER BY dateCreation DESC",
|
||||
conversationId
|
||||
)
|
||||
.page(0, limit)
|
||||
.list();
|
||||
public Optional<Message> findMessageById(UUID id) {
|
||||
return find("id", id).firstResultOptional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte les messages non lus d'une conversation pour un membre
|
||||
* Récupère les messages d'une conversation, paginés, du plus récent au plus ancien.
|
||||
*
|
||||
* @param conversationId ID de la conversation
|
||||
* @param page numéro de page (0-based)
|
||||
* @param size nombre de messages par page
|
||||
*/
|
||||
public long countUnreadByConversationAndMember(UUID conversationId, UUID membreId) {
|
||||
// Pour simplifier, on compte les messages SENT/DELIVERED (pas READ)
|
||||
// et dont le sender n'est PAS le membre en question
|
||||
public List<Message> findByConversationPagine(UUID conversationId, int page, int size) {
|
||||
return find(
|
||||
"conversation.id = ?1 AND actif = true ORDER BY dateCreation DESC",
|
||||
conversationId
|
||||
).page(Page.of(page, size)).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte les messages non lus dans une conversation pour un membre donné.
|
||||
* Un message est non lu si sa dateCreation est postérieure au luJusqua du participant.
|
||||
*/
|
||||
public long countNonLus(UUID conversationId, UUID membreId) {
|
||||
return count(
|
||||
"conversation.id = ?1 AND sender.id != ?2 AND status IN ('SENT', 'DELIVERED') AND isDeleted = false",
|
||||
conversationId,
|
||||
membreId
|
||||
"SELECT COUNT(m) FROM Message m, ConversationParticipant p " +
|
||||
"WHERE m.conversation.id = ?1 " +
|
||||
"AND p.conversation.id = ?1 " +
|
||||
"AND p.membre.id = ?2 " +
|
||||
"AND m.actif = true " +
|
||||
"AND (p.luJusqua IS NULL OR m.dateCreation > p.luJusqua) " +
|
||||
"AND m.expediteur.id <> ?2",
|
||||
conversationId, membreId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque tous les messages d'une conversation comme lus pour un membre
|
||||
* Récupère les messages non supprimés d'une conversation (pour les tests).
|
||||
*/
|
||||
public int markAllAsReadByConversationAndMember(UUID conversationId, UUID membreId) {
|
||||
return update(
|
||||
"status = 'READ', readAt = CURRENT_TIMESTAMP WHERE conversation.id = ?1 AND sender.id != ?2 AND status != 'READ' AND isDeleted = false",
|
||||
conversationId,
|
||||
membreId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve le dernier message d'une conversation
|
||||
*/
|
||||
public Message findLastByConversation(UUID conversationId) {
|
||||
public List<Message> findActifsByConversation(UUID conversationId) {
|
||||
return find(
|
||||
"conversation.id = ?1 AND isDeleted = false ORDER BY dateCreation DESC",
|
||||
"conversation.id = ?1 AND actif = true ORDER BY dateCreation ASC",
|
||||
conversationId
|
||||
)
|
||||
.firstResult();
|
||||
).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve le dernier message actif d'une conversation.
|
||||
*/
|
||||
public Optional<Message> findDernierMessage(UUID conversationId) {
|
||||
return find(
|
||||
"conversation.id = ?1 AND actif = true ORDER BY dateCreation DESC",
|
||||
conversationId
|
||||
).firstResultOptional();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement;
|
||||
import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement;
|
||||
import dev.lions.unionflow.server.entity.Paiement;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import io.quarkus.panache.common.Page;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.math.BigDecimal;
|
||||
@@ -14,7 +11,7 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository pour l'entité Paiement
|
||||
* Repository pour l'entité Paiement.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
@@ -23,90 +20,57 @@ import java.util.UUID;
|
||||
@ApplicationScoped
|
||||
public class PaiementRepository implements PanacheRepositoryBase<Paiement, UUID> {
|
||||
|
||||
/**
|
||||
* Trouve un paiement par son UUID
|
||||
*
|
||||
* @param id UUID du paiement
|
||||
* @return Paiement ou Optional.empty()
|
||||
*/
|
||||
/** Trouve un paiement actif par son UUID. */
|
||||
public Optional<Paiement> findPaiementById(UUID id) {
|
||||
return find("id = ?1 AND actif = true", id).firstResultOptional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un paiement par son numéro de référence
|
||||
*
|
||||
* @param numeroReference Numéro de référence
|
||||
* @return Paiement ou Optional.empty()
|
||||
*/
|
||||
/** Trouve un paiement par son numéro de référence. */
|
||||
public Optional<Paiement> findByNumeroReference(String numeroReference) {
|
||||
return find("numeroReference", numeroReference).firstResultOptional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve tous les paiements d'un membre
|
||||
*
|
||||
* @param membreId ID du membre
|
||||
* @return Liste des paiements
|
||||
*/
|
||||
/** Tous les paiements actifs d'un membre, triés par date décroissante. */
|
||||
public List<Paiement> findByMembreId(UUID membreId) {
|
||||
return find("membre.id = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), membreId)
|
||||
return find(
|
||||
"membre.id = ?1 AND actif = true",
|
||||
Sort.by("datePaiement", Sort.Direction.Descending),
|
||||
membreId)
|
||||
.list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve les paiements par statut
|
||||
*
|
||||
* @param statut Statut du paiement
|
||||
* @return Liste des paiements
|
||||
*/
|
||||
public List<Paiement> findByStatut(StatutPaiement statut) {
|
||||
return find("statutPaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), statut.name())
|
||||
/** Paiements actifs par statut (valeur String), triés par date décroissante. */
|
||||
public List<Paiement> findByStatut(String statut) {
|
||||
return find(
|
||||
"statutPaiement = ?1 AND actif = true",
|
||||
Sort.by("datePaiement", Sort.Direction.Descending),
|
||||
statut)
|
||||
.list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve les paiements par méthode
|
||||
*
|
||||
* @param methode Méthode de paiement
|
||||
* @return Liste des paiements
|
||||
*/
|
||||
public List<Paiement> findByMethode(MethodePaiement methode) {
|
||||
return find("methodePaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), methode.name())
|
||||
/** Paiements actifs par méthode (valeur String), triés par date décroissante. */
|
||||
public List<Paiement> findByMethode(String methode) {
|
||||
return find(
|
||||
"methodePaiement = ?1 AND actif = true",
|
||||
Sort.by("datePaiement", Sort.Direction.Descending),
|
||||
methode)
|
||||
.list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve les paiements validés dans une période
|
||||
*
|
||||
* @param dateDebut Date de début
|
||||
* @param dateFin Date de fin
|
||||
* @return Liste des paiements
|
||||
*/
|
||||
/** Paiements validés dans une période, triés par date de validation décroissante. */
|
||||
public List<Paiement> findValidesParPeriode(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
return find(
|
||||
"statutPaiement = ?1 AND dateValidation >= ?2 AND dateValidation <= ?3 AND actif = true",
|
||||
"statutPaiement = 'VALIDE' AND dateValidation >= ?1 AND dateValidation <= ?2 AND actif = true",
|
||||
Sort.by("dateValidation", Sort.Direction.Descending),
|
||||
StatutPaiement.VALIDE.name(),
|
||||
dateDebut,
|
||||
dateFin)
|
||||
.list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le montant total des paiements validés dans une période
|
||||
*
|
||||
* @param dateDebut Date de début
|
||||
* @param dateFin Date de fin
|
||||
* @return Montant total
|
||||
*/
|
||||
/** Somme des montants validés sur une période. */
|
||||
public BigDecimal calculerMontantTotalValides(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
List<Paiement> paiements = findValidesParPeriode(dateDebut, dateFin);
|
||||
return paiements.stream()
|
||||
return findValidesParPeriode(dateDebut, dateFin).stream()
|
||||
.map(Paiement::getMontant)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.SystemConfigPersistence;
|
||||
import io.quarkus.arc.Unremovable;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Repository pour la persistance des paramètres système en base de données.
|
||||
* Remplace le stockage AtomicReference en RAM pour les clés critiques
|
||||
* (maintenance_mode, scheduled_maintenance, etc.).
|
||||
*
|
||||
* Étend BaseRepository pour cohérence avec le reste du projet et accès
|
||||
* à EntityManager.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Unremovable
|
||||
public class SystemConfigPersistenceRepository extends BaseRepository<SystemConfigPersistence> {
|
||||
|
||||
public SystemConfigPersistenceRepository() {
|
||||
super(SystemConfigPersistence.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cherche une entrée de configuration par clé.
|
||||
*/
|
||||
public Optional<SystemConfigPersistence> findByKey(String key) {
|
||||
return find("configKey", key).firstResultOptional();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée ou met à jour une valeur de configuration.
|
||||
*/
|
||||
@Transactional
|
||||
public void setValue(String key, String value) {
|
||||
Optional<SystemConfigPersistence> existing = findByKey(key);
|
||||
if (existing.isPresent()) {
|
||||
existing.get().setConfigValue(value);
|
||||
persist(existing.get());
|
||||
} else {
|
||||
persist(SystemConfigPersistence.builder()
|
||||
.configKey(key)
|
||||
.configValue(value)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la valeur d'une clé, ou {@code defaultValue} si absente.
|
||||
*/
|
||||
public String getValue(String key, String defaultValue) {
|
||||
return findByKey(key)
|
||||
.map(SystemConfigPersistence::getConfigValue)
|
||||
.orElse(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la valeur booléenne d'une clé, ou {@code defaultValue} si absente.
|
||||
*/
|
||||
public boolean getBooleanValue(String key, boolean defaultValue) {
|
||||
String val = getValue(key, null);
|
||||
return val != null ? Boolean.parseBoolean(val) : defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import io.quarkus.panache.common.Page;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -150,8 +151,10 @@ public class SystemLogRepository extends BaseRepository<SystemLog> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer les logs plus anciens qu'une date donnée (rotation)
|
||||
* Supprimer les logs plus anciens qu'une date donnée (rotation).
|
||||
* Requiert une transaction active — DELETE via JPQL doit être transactionnel.
|
||||
*/
|
||||
@Transactional
|
||||
public int deleteOlderThan(LocalDateTime threshold) {
|
||||
return entityManager.createQuery(
|
||||
"DELETE FROM SystemLog l WHERE l.timestamp < :threshold"
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement;
|
||||
import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement;
|
||||
import dev.lions.unionflow.server.entity.Versement;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
|
||||
import io.quarkus.panache.common.Sort;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository pour l'entité {@link Versement}.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class VersementRepository implements PanacheRepositoryBase<Versement, UUID> {
|
||||
|
||||
/** Trouve un versement actif par UUID. */
|
||||
public Optional<Versement> findVersementById(UUID id) {
|
||||
return find("id = ?1 AND actif = true", id).firstResultOptional();
|
||||
}
|
||||
|
||||
/** Trouve un versement par son numéro de référence. */
|
||||
public Optional<Versement> findByNumeroReference(String numeroReference) {
|
||||
return find("numeroReference", numeroReference).firstResultOptional();
|
||||
}
|
||||
|
||||
/** Liste tous les versements actifs d'un membre, les plus récents d'abord. */
|
||||
public List<Versement> findByMembreId(UUID membreId) {
|
||||
return find(
|
||||
"membre.id = ?1 AND actif = true",
|
||||
Sort.by("datePaiement", Sort.Direction.Descending),
|
||||
membreId
|
||||
).list();
|
||||
}
|
||||
|
||||
/** Liste les versements par statut. */
|
||||
public List<Versement> findByStatut(StatutPaiement statut) {
|
||||
return find(
|
||||
"statutPaiement = ?1 AND actif = true",
|
||||
Sort.by("datePaiement", Sort.Direction.Descending),
|
||||
statut.name()
|
||||
).list();
|
||||
}
|
||||
|
||||
/** Liste les versements par méthode. */
|
||||
public List<Versement> findByMethode(MethodePaiement methode) {
|
||||
return find(
|
||||
"methodePaiement = ?1 AND actif = true",
|
||||
Sort.by("datePaiement", Sort.Direction.Descending),
|
||||
methode.name()
|
||||
).list();
|
||||
}
|
||||
|
||||
/** Liste les versements confirmés dans une période donnée. */
|
||||
public List<Versement> findConfirmesParPeriode(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
return find(
|
||||
"statutPaiement IN ('CONFIRME', 'VALIDE') "
|
||||
+ "AND dateValidation >= ?1 AND dateValidation <= ?2 AND actif = true",
|
||||
Sort.by("dateValidation", Sort.Direction.Descending),
|
||||
dateDebut,
|
||||
dateFin
|
||||
).list();
|
||||
}
|
||||
|
||||
/** Calcule le montant total des versements confirmés dans une période. */
|
||||
public BigDecimal calculerMontantTotalConfirmes(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
return findConfirmesParPeriode(dateDebut, dateFin).stream()
|
||||
.map(Versement::getMontant)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse;
|
||||
import dev.lions.unionflow.server.service.ConversationService;
|
||||
import dev.lions.unionflow.server.service.support.SecuriteHelper;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
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.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Resource REST pour la gestion des conversations
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-16
|
||||
*/
|
||||
@Path("/api/conversations")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Communication", description = "Gestion des conversations et messages")
|
||||
@RolesAllowed({"ADMIN", "SUPER_ADMIN", "MEMBRE", "ADMIN_ORGANISATION"})
|
||||
public class ConversationResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ConversationResource.class);
|
||||
|
||||
@Inject
|
||||
ConversationService conversationService;
|
||||
|
||||
@Inject
|
||||
SecuriteHelper securiteHelper;
|
||||
|
||||
/**
|
||||
* Liste les conversations de l'utilisateur connecté
|
||||
*/
|
||||
@GET
|
||||
@Operation(summary = "Lister mes conversations")
|
||||
@APIResponse(responseCode = "200", description = "Liste des conversations")
|
||||
public Response getConversations(
|
||||
@Parameter(description = "Inclure conversations archivées")
|
||||
@QueryParam("includeArchived") @DefaultValue("false") boolean includeArchived,
|
||||
@Parameter(description = "Filtrer par organisation")
|
||||
@QueryParam("organisationId") String organisationId
|
||||
) {
|
||||
UUID membreId = securiteHelper.resolveMembreId();
|
||||
UUID orgId = organisationId != null ? UUID.fromString(organisationId) : null;
|
||||
|
||||
List<ConversationResponse> conversations = conversationService.getConversations(membreId, orgId, includeArchived);
|
||||
return Response.ok(conversations).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une conversation par ID
|
||||
*/
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@Operation(summary = "Récupérer une conversation")
|
||||
@APIResponse(responseCode = "200", description = "Conversation trouvée")
|
||||
@APIResponse(responseCode = "404", description = "Conversation non trouvée")
|
||||
public Response getConversationById(@PathParam("id") UUID conversationId) {
|
||||
UUID membreId = securiteHelper.resolveMembreId();
|
||||
ConversationResponse conversation = conversationService.getConversationById(conversationId, membreId);
|
||||
return Response.ok(conversation).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle conversation
|
||||
*/
|
||||
@POST
|
||||
@Operation(summary = "Créer une conversation")
|
||||
@APIResponse(responseCode = "201", description = "Conversation créée")
|
||||
@APIResponse(responseCode = "400", description = "Données invalides")
|
||||
public Response createConversation(@Valid CreateConversationRequest request) {
|
||||
UUID creatorId = securiteHelper.resolveMembreId();
|
||||
ConversationResponse conversation = conversationService.createConversation(request, creatorId);
|
||||
return Response.status(Response.Status.CREATED).entity(conversation).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive une conversation
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/archive")
|
||||
@Operation(summary = "Archiver/désarchiver une conversation")
|
||||
@APIResponse(responseCode = "204", description = "Conversation archivée")
|
||||
public Response archiveConversation(
|
||||
@PathParam("id") UUID conversationId,
|
||||
@QueryParam("archive") @DefaultValue("true") boolean archive
|
||||
) {
|
||||
UUID membreId = securiteHelper.resolveMembreId();
|
||||
conversationService.archiveConversation(conversationId, membreId, archive);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque une conversation comme lue
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/mark-read")
|
||||
@Operation(summary = "Marquer conversation comme lue")
|
||||
@APIResponse(responseCode = "204", description = "Marquée comme lue")
|
||||
public Response markAsRead(@PathParam("id") UUID conversationId) {
|
||||
UUID membreId = securiteHelper.resolveMembreId();
|
||||
conversationService.markAsRead(conversationId, membreId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle mute conversation
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/toggle-mute")
|
||||
@Operation(summary = "Activer/désactiver le son")
|
||||
@APIResponse(responseCode = "204", description = "Paramètre modifié")
|
||||
public Response toggleMute(@PathParam("id") UUID conversationId) {
|
||||
UUID membreId = securiteHelper.resolveMembreId();
|
||||
conversationService.toggleMute(conversationId, membreId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle pin conversation
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}/toggle-pin")
|
||||
@Operation(summary = "Épingler/désépingler")
|
||||
@APIResponse(responseCode = "204", description = "Paramètre modifié")
|
||||
public Response togglePin(@PathParam("id") UUID conversationId) {
|
||||
UUID membreId = securiteHelper.resolveMembreId();
|
||||
conversationService.togglePin(conversationId, membreId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest;
|
||||
import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
|
||||
import dev.lions.unionflow.server.service.MessageService;
|
||||
import dev.lions.unionflow.server.service.support.SecuriteHelper;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
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.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Resource REST pour la gestion des messages
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-16
|
||||
*/
|
||||
@Path("/api/messages")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Communication", description = "Gestion des conversations et messages")
|
||||
@RolesAllowed({"ADMIN", "SUPER_ADMIN", "MEMBRE", "ADMIN_ORGANISATION"})
|
||||
public class MessageResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MessageResource.class);
|
||||
|
||||
@Inject
|
||||
MessageService messageService;
|
||||
|
||||
@Inject
|
||||
SecuriteHelper securiteHelper;
|
||||
|
||||
/**
|
||||
* Récupère les messages d'une conversation
|
||||
*/
|
||||
@GET
|
||||
@Operation(summary = "Lister les messages d'une conversation")
|
||||
@APIResponse(responseCode = "200", description = "Liste des messages")
|
||||
@APIResponse(responseCode = "404", description = "Conversation non trouvée")
|
||||
public Response getMessages(
|
||||
@Parameter(description = "ID de la conversation", required = true)
|
||||
@QueryParam("conversationId") UUID conversationId,
|
||||
@Parameter(description = "Nombre maximum de messages")
|
||||
@QueryParam("limit") @DefaultValue("50") int limit
|
||||
) {
|
||||
if (conversationId == null) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "conversationId requis"))
|
||||
.build();
|
||||
}
|
||||
|
||||
UUID membreId = securiteHelper.resolveMembreId();
|
||||
List<MessageResponse> messages = messageService.getMessages(conversationId, membreId, limit);
|
||||
return Response.ok(messages).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un message
|
||||
*/
|
||||
@POST
|
||||
@Operation(summary = "Envoyer un message")
|
||||
@APIResponse(responseCode = "201", description = "Message envoyé")
|
||||
@APIResponse(responseCode = "400", description = "Données invalides")
|
||||
@APIResponse(responseCode = "404", description = "Conversation non trouvée")
|
||||
public Response sendMessage(@Valid SendMessageRequest request) {
|
||||
UUID senderId = securiteHelper.resolveMembreId();
|
||||
MessageResponse message = messageService.sendMessage(request, senderId);
|
||||
return Response.status(Response.Status.CREATED).entity(message).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Édite un message
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{id}")
|
||||
@Operation(summary = "Éditer un message")
|
||||
@APIResponse(responseCode = "200", description = "Message édité")
|
||||
@APIResponse(responseCode = "404", description = "Message non trouvé")
|
||||
public Response editMessage(
|
||||
@PathParam("id") UUID messageId,
|
||||
Map<String, String> body
|
||||
) {
|
||||
String newContent = body.get("content");
|
||||
if (newContent == null || newContent.isEmpty()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "content requis"))
|
||||
.build();
|
||||
}
|
||||
|
||||
UUID senderId = securiteHelper.resolveMembreId();
|
||||
MessageResponse message = messageService.editMessage(messageId, senderId, newContent);
|
||||
return Response.ok(message).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un message
|
||||
*/
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Operation(summary = "Supprimer un message")
|
||||
@APIResponse(responseCode = "204", description = "Message supprimé")
|
||||
@APIResponse(responseCode = "404", description = "Message non trouvé")
|
||||
public Response deleteMessage(@PathParam("id") UUID messageId) {
|
||||
UUID senderId = securiteHelper.resolveMembreId();
|
||||
messageService.deleteMessage(messageId, senderId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.BloquerMembreRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationDirecteRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationRoleRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.EnvoyerMessageRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.MettreAJourPolitiqueRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse;
|
||||
import dev.lions.unionflow.server.service.MessagingService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Resource REST pour la messagerie instantanée.
|
||||
*
|
||||
* <p>Endpoints :
|
||||
* <ul>
|
||||
* <li>POST /api/messagerie/conversations/directe — démarrer conversation 1-1</li>
|
||||
* <li>POST /api/messagerie/conversations/role — contacter un rôle officiel</li>
|
||||
* <li>GET /api/messagerie/conversations — mes conversations</li>
|
||||
* <li>GET /api/messagerie/conversations/{id} — détail + messages</li>
|
||||
* <li>DELETE /api/messagerie/conversations/{id} — archiver</li>
|
||||
* <li>POST /api/messagerie/conversations/{id}/messages — envoyer un message</li>
|
||||
* <li>GET /api/messagerie/conversations/{id}/messages — historique</li>
|
||||
* <li>PUT /api/messagerie/conversations/{id}/lire — marquer comme lu</li>
|
||||
* <li>DELETE /api/messagerie/conversations/{cId}/messages/{mId} — supprimer message</li>
|
||||
* <li>POST /api/messagerie/blocages — bloquer un membre</li>
|
||||
* <li>DELETE /api/messagerie/blocages/{membreId} — débloquer</li>
|
||||
* <li>GET /api/messagerie/blocages — mes blocages</li>
|
||||
* <li>GET /api/messagerie/politique/{orgId} — politique de communication</li>
|
||||
* <li>PUT /api/messagerie/politique/{orgId} — mettre à jour (ADMIN)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Path("/api/messagerie")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
@Tag(name = "Messagerie", description = "Messagerie instantanée — conversations, messages, notes vocales")
|
||||
public class MessagingResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MessagingResource.class);
|
||||
|
||||
@Inject
|
||||
MessagingService messagingService;
|
||||
|
||||
// ── Conversations ─────────────────────────────────────────────────────────
|
||||
|
||||
@POST
|
||||
@Path("/conversations/directe")
|
||||
public Response demarrerConversationDirecte(@Valid DemarrerConversationDirecteRequest request) {
|
||||
LOG.infof("POST /api/messagerie/conversations/directe → destinataire: %s", request.destinataireId());
|
||||
ConversationResponse result = messagingService.demarrerConversationDirecte(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/conversations/role")
|
||||
public Response demarrerConversationRole(@Valid DemarrerConversationRoleRequest request) {
|
||||
LOG.infof("POST /api/messagerie/conversations/role → rôle: %s, org: %s",
|
||||
request.roleCible(), request.organisationId());
|
||||
ConversationResponse result = messagingService.demarrerConversationRole(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/conversations")
|
||||
public Response getMesConversations() {
|
||||
LOG.debug("GET /api/messagerie/conversations");
|
||||
List<ConversationSummaryResponse> result = messagingService.getMesConversations();
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/conversations/{id}")
|
||||
public Response getConversation(@PathParam("id") UUID id) {
|
||||
LOG.infof("GET /api/messagerie/conversations/%s", id);
|
||||
ConversationResponse result = messagingService.getConversation(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/conversations/{id}")
|
||||
public Response archiverConversation(@PathParam("id") UUID id) {
|
||||
LOG.infof("DELETE /api/messagerie/conversations/%s", id);
|
||||
ConversationResponse result = messagingService.archiverConversation(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
// ── Messages ──────────────────────────────────────────────────────────────
|
||||
|
||||
@POST
|
||||
@Path("/conversations/{id}/messages")
|
||||
public Response envoyerMessage(
|
||||
@PathParam("id") UUID conversationId,
|
||||
@Valid EnvoyerMessageRequest request) {
|
||||
LOG.infof("POST /api/messagerie/conversations/%s/messages", conversationId);
|
||||
MessageResponse result = messagingService.envoyerMessage(conversationId, request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/conversations/{id}/messages")
|
||||
public Response getMessages(
|
||||
@PathParam("id") UUID conversationId,
|
||||
@QueryParam("page") @DefaultValue("0") int page) {
|
||||
LOG.infof("GET /api/messagerie/conversations/%s/messages?page=%d", conversationId, page);
|
||||
List<MessageResponse> result = messagingService.getMessages(conversationId, page);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/conversations/{id}/lire")
|
||||
public Response marquerLu(@PathParam("id") UUID conversationId) {
|
||||
LOG.infof("PUT /api/messagerie/conversations/%s/lire", conversationId);
|
||||
messagingService.marquerConversationLue(conversationId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/conversations/{conversationId}/messages/{messageId}")
|
||||
public Response supprimerMessage(
|
||||
@PathParam("conversationId") UUID conversationId,
|
||||
@PathParam("messageId") UUID messageId) {
|
||||
LOG.infof("DELETE /api/messagerie/conversations/%s/messages/%s", conversationId, messageId);
|
||||
messagingService.supprimerMessage(conversationId, messageId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
// ── Blocages ──────────────────────────────────────────────────────────────
|
||||
|
||||
@POST
|
||||
@Path("/blocages")
|
||||
public Response bloquerMembre(@Valid BloquerMembreRequest request) {
|
||||
LOG.infof("POST /api/messagerie/blocages → bloquer: %s", request.membreABloquerId());
|
||||
messagingService.bloquerMembre(request);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/blocages/{membreId}")
|
||||
public Response debloquerMembre(
|
||||
@PathParam("membreId") UUID membreId,
|
||||
@QueryParam("organisationId") UUID organisationId) {
|
||||
LOG.infof("DELETE /api/messagerie/blocages/%s", membreId);
|
||||
messagingService.debloquerMembre(membreId, organisationId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/blocages")
|
||||
public Response getMesBlocages() {
|
||||
LOG.debug("GET /api/messagerie/blocages");
|
||||
return Response.ok(messagingService.getMesBlocages()).build();
|
||||
}
|
||||
|
||||
// ── Politique de communication ────────────────────────────────────────────
|
||||
|
||||
@GET
|
||||
@Path("/politique/{organisationId}")
|
||||
public Response getPolitique(@PathParam("organisationId") UUID organisationId) {
|
||||
LOG.infof("GET /api/messagerie/politique/%s", organisationId);
|
||||
ContactPolicyResponse result = messagingService.getPolitique(organisationId);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/politique/{organisationId}")
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
|
||||
public Response mettreAJourPolitique(
|
||||
@PathParam("organisationId") UUID organisationId,
|
||||
@Valid MettreAJourPolitiqueRequest request) {
|
||||
LOG.infof("PUT /api/messagerie/politique/%s", organisationId);
|
||||
ContactPolicyResponse result = messagingService.mettreAJourPolitique(organisationId, request);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.paiement.request.CreatePaiementRequest;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.request.DeclarerPaiementManuelRequest;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse;
|
||||
import dev.lions.unionflow.server.service.PaiementService;
|
||||
@@ -16,193 +20,138 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Resource REST pour la gestion des paiements
|
||||
* Resource REST pour la gestion des paiements (Wave Checkout et paiements manuels).
|
||||
*
|
||||
* <p>Endpoints principaux :
|
||||
* <ul>
|
||||
* <li>{@code POST /api/paiements/initier-paiement-en-ligne} — démarre le flux Wave QR code</li>
|
||||
* <li>{@code GET /api/paiements/statut-intention/{intentionId}} — polling du statut Wave</li>
|
||||
* <li>{@code POST /api/paiements/declarer-manuel} — paiement manuel (espèces/virement)</li>
|
||||
* <li>{@code GET /api/paiements/mon-historique} — historique du membre connecté</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 3.0
|
||||
* @since 2025-01-29
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Path("/api/paiements")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER" })
|
||||
@Tag(name = "Paiements", description = "Gestion des paiements : création, validation et suivi")
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER"})
|
||||
@Tag(name = "Paiements", description = "Paiements de cotisations — Wave Checkout et manuel")
|
||||
public class PaiementResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(PaiementResource.class);
|
||||
private static final Logger LOG = Logger.getLogger(PaiementResource.class);
|
||||
|
||||
@Inject
|
||||
PaiementService paiementService;
|
||||
@Inject
|
||||
PaiementService paiementService;
|
||||
|
||||
/**
|
||||
* Crée un nouveau paiement
|
||||
*
|
||||
* @param request DTO du paiement à créer
|
||||
* @return Paiement créé
|
||||
*/
|
||||
@POST
|
||||
@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "MEMBRE" })
|
||||
public Response creerPaiement(@Valid CreatePaiementRequest request) {
|
||||
LOG.infof("POST /api/paiements - Création paiement: %s", request.numeroReference());
|
||||
PaiementResponse result = paiementService.creerPaiement(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
// ── Lecture ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Valide un paiement
|
||||
*
|
||||
* @param id ID du paiement
|
||||
* @return Paiement validé
|
||||
*/
|
||||
@POST
|
||||
@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "MEMBRE" })
|
||||
@Path("/{id}/valider")
|
||||
public Response validerPaiement(@PathParam("id") UUID id) {
|
||||
LOG.infof("POST /api/paiements/%s/valider", id);
|
||||
PaiementResponse result = paiementService.validerPaiement(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public Response trouverParId(@PathParam("id") UUID id) {
|
||||
LOG.infof("GET /api/paiements/%s", id);
|
||||
PaiementResponse result = paiementService.trouverParId(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule un paiement
|
||||
*
|
||||
* @param id ID du paiement
|
||||
* @return Paiement annulé
|
||||
*/
|
||||
@POST
|
||||
@RolesAllowed({ "ADMIN", "ADMIN_ORGANISATION", "MEMBRE" })
|
||||
@Path("/{id}/annuler")
|
||||
public Response annulerPaiement(@PathParam("id") UUID id) {
|
||||
LOG.infof("POST /api/paiements/%s/annuler", id);
|
||||
PaiementResponse result = paiementService.annulerPaiement(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
@GET
|
||||
@Path("/reference/{numeroReference}")
|
||||
public Response trouverParNumeroReference(
|
||||
@PathParam("numeroReference") String numeroReference) {
|
||||
LOG.infof("GET /api/paiements/reference/%s", numeroReference);
|
||||
PaiementResponse result = paiementService.trouverParNumeroReference(numeroReference);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un paiement par son ID
|
||||
*
|
||||
* @param id ID du paiement
|
||||
* @return Paiement
|
||||
*/
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public Response trouverParId(@PathParam("id") UUID id) {
|
||||
LOG.infof("GET /api/paiements/%s", id);
|
||||
PaiementResponse result = paiementService.trouverParId(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
@GET
|
||||
@Path("/membre/{membreId}")
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
|
||||
public Response listerParMembre(@PathParam("membreId") UUID membreId) {
|
||||
LOG.infof("GET /api/paiements/membre/%s", membreId);
|
||||
List<PaiementSummaryResponse> result = paiementService.listerParMembre(membreId);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un paiement par son numéro de référence
|
||||
*
|
||||
* @param numeroReference Numéro de référence
|
||||
* @return Paiement
|
||||
*/
|
||||
@GET
|
||||
@Path("/reference/{numeroReference}")
|
||||
public Response trouverParNumeroReference(@PathParam("numeroReference") String numeroReference) {
|
||||
LOG.infof("GET /api/paiements/reference/%s", numeroReference);
|
||||
PaiementResponse result = paiementService.trouverParNumeroReference(numeroReference);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
@GET
|
||||
@Path("/mon-historique")
|
||||
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION"})
|
||||
public Response getMonHistoriquePaiements(
|
||||
@QueryParam("limit") @DefaultValue("20") int limit) {
|
||||
LOG.infof("GET /api/paiements/mon-historique?limit=%d", limit);
|
||||
List<PaiementSummaryResponse> result = paiementService.getMonHistoriquePaiements(limit);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste tous les paiements d'un membre
|
||||
*
|
||||
* @param membreId ID du membre
|
||||
* @return Liste des paiements
|
||||
*/
|
||||
@GET
|
||||
@Path("/membre/{membreId}")
|
||||
public Response listerParMembre(@PathParam("membreId") UUID membreId) {
|
||||
LOG.infof("GET /api/paiements/membre/%s", membreId);
|
||||
List<PaiementSummaryResponse> result = paiementService.listerParMembre(membreId);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
// ── Administration ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Liste l'historique des paiements du membre connecté (auto-détection).
|
||||
* Utilisé par la page personnelle "Payer mes Cotisations".
|
||||
*
|
||||
* @param limit Nombre maximum de paiements à retourner (défaut : 5)
|
||||
* @return Liste des derniers paiements
|
||||
*/
|
||||
@GET
|
||||
@Path("/mes-paiements/historique")
|
||||
@RolesAllowed({ "MEMBRE", "ADMIN", "ADMIN_ORGANISATION" })
|
||||
public Response getMonHistoriquePaiements(
|
||||
@QueryParam("limit") @DefaultValue("5") int limit) {
|
||||
LOG.infof("GET /api/paiements/mes-paiements/historique?limit=%d", limit);
|
||||
List<PaiementSummaryResponse> result = paiementService.getMonHistoriquePaiements(limit);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
@POST
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
|
||||
public Response creerPaiement(@Valid CreatePaiementRequest request) {
|
||||
LOG.infof("POST /api/paiements — référence: %s", request.numeroReference());
|
||||
PaiementResponse result = paiementService.creerPaiement(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initie un paiement en ligne via un gateway (Wave, Orange Money, Free Money, Carte).
|
||||
* Retourne l'URL de redirection vers le gateway.
|
||||
*
|
||||
* @param request Données du paiement en ligne
|
||||
* @return URL de redirection + transaction ID
|
||||
*/
|
||||
@POST
|
||||
@Path("/initier-paiement-en-ligne")
|
||||
@RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" })
|
||||
public Response initierPaiementEnLigne(@Valid dev.lions.unionflow.server.api.dto.paiement.request.InitierPaiementEnLigneRequest request) {
|
||||
LOG.infof("POST /api/paiements/initier-paiement-en-ligne - cotisation: %s, méthode: %s",
|
||||
request.cotisationId(), request.methodePaiement());
|
||||
dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse result =
|
||||
paiementService.initierPaiementEnLigne(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
@POST
|
||||
@Path("/{id}/valider")
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
|
||||
public Response validerPaiement(@PathParam("id") UUID id) {
|
||||
LOG.infof("POST /api/paiements/%s/valider", id);
|
||||
PaiementResponse result = paiementService.validerPaiement(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initie un dépôt sur compte épargne via Wave (même flux que cotisations).
|
||||
* Retourne wave_launch_url pour ouvrir l'app Wave puis retour deep link.
|
||||
*/
|
||||
@POST
|
||||
@Path("/initier-depot-epargne-en-ligne")
|
||||
@RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" })
|
||||
public Response initierDepotEpargneEnLigne(@Valid dev.lions.unionflow.server.api.dto.paiement.request.InitierDepotEpargneRequest request) {
|
||||
LOG.infof("POST /api/paiements/initier-depot-epargne-en-ligne - compte: %s, montant: %s",
|
||||
request.compteId(), request.montant());
|
||||
dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse result =
|
||||
paiementService.initierDepotEpargneEnLigne(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
@POST
|
||||
@Path("/{id}/annuler")
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
public Response annulerPaiement(@PathParam("id") UUID id) {
|
||||
LOG.infof("POST /api/paiements/%s/annuler", id);
|
||||
PaiementResponse result = paiementService.annulerPaiement(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Polling du statut d'une IntentionPaiement Wave.
|
||||
* Si Wave a confirmé le paiement, réconcilie automatiquement la cotisation (PAYEE) et retourne COMPLETEE.
|
||||
* Le client web appelle cet endpoint toutes les 3 secondes pendant l'affichage du QR code.
|
||||
*
|
||||
* @param intentionId UUID de l'intention (clientReference retourné par initier-paiement-en-ligne)
|
||||
* @return Statut courant + waveLaunchUrl (pour re-générer le QR si besoin) + message
|
||||
*/
|
||||
@GET
|
||||
@Path("/statut-intention/{intentionId}")
|
||||
@RolesAllowed({ "MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER" })
|
||||
public Response getStatutIntention(@PathParam("intentionId") java.util.UUID intentionId) {
|
||||
LOG.infof("GET /api/paiements/statut-intention/%s", intentionId);
|
||||
dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse result =
|
||||
paiementService.verifierStatutIntention(intentionId);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
// ── Flux Wave Checkout (QR code web) ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Déclare un paiement manuel (espèces, virement, chèque).
|
||||
* Le paiement est créé avec le statut EN_ATTENTE_VALIDATION.
|
||||
* Le trésorier devra le valider via une page admin.
|
||||
*
|
||||
* @param request Données du paiement manuel
|
||||
* @return Paiement créé (statut EN_ATTENTE_VALIDATION)
|
||||
*/
|
||||
@POST
|
||||
@Path("/declarer-paiement-manuel")
|
||||
@RolesAllowed({ "MEMBRE", "ADMIN", "ADMIN_ORGANISATION" })
|
||||
public Response declarerPaiementManuel(@Valid dev.lions.unionflow.server.api.dto.paiement.request.DeclarerPaiementManuelRequest request) {
|
||||
LOG.infof("POST /api/paiements/declarer-paiement-manuel - cotisation: %s, méthode: %s",
|
||||
request.cotisationId(), request.methodePaiement());
|
||||
PaiementResponse result = paiementService.declarerPaiementManuel(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
/**
|
||||
* Initie un paiement Wave via Checkout QR code.
|
||||
* Le web encode le {@code waveLaunchUrl} en QR code, l'utilisateur le scanne
|
||||
* depuis l'app Wave. Après confirmation, Wave redirige vers la success URL.
|
||||
*/
|
||||
@POST
|
||||
@Path("/initier-paiement-en-ligne")
|
||||
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION", "USER"})
|
||||
public Response initierPaiementEnLigne(@Valid InitierPaiementEnLigneRequest request) {
|
||||
LOG.infof("POST /api/paiements/initier-paiement-en-ligne — cotisation: %s, méthode: %s",
|
||||
request.cotisationId(), request.methodePaiement());
|
||||
PaiementGatewayResponse result = paiementService.initierPaiementEnLigne(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Polling du statut d'une intention de paiement Wave.
|
||||
* Appelé toutes les 3 secondes par le web pendant que l'utilisateur scanne le QR code.
|
||||
* Retourne {@code confirme=true} dès que Wave confirme le paiement.
|
||||
*/
|
||||
@GET
|
||||
@Path("/statut-intention/{intentionId}")
|
||||
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION", "USER"})
|
||||
public Response getStatutIntention(@PathParam("intentionId") UUID intentionId) {
|
||||
LOG.infof("GET /api/paiements/statut-intention/%s", intentionId);
|
||||
IntentionStatutResponse result = paiementService.getStatutIntention(intentionId);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
// ── Flux manuel ───────────────────────────────────────────────────────────
|
||||
|
||||
@POST
|
||||
@Path("/declarer-manuel")
|
||||
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION"})
|
||||
public Response declarerPaiementManuel(@Valid DeclarerPaiementManuelRequest request) {
|
||||
LOG.infof("POST /api/paiements/declarer-manuel — cotisation: %s, méthode: %s",
|
||||
request.cotisationId(), request.methodePaiement());
|
||||
PaiementResponse result = paiementService.declarerPaiementManuel(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion de la configuration système
|
||||
*/
|
||||
@@ -120,4 +122,172 @@ public class SystemResource {
|
||||
log.info("GET /api/system/metrics");
|
||||
return systemMetricsService.getSystemMetrics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimiser la base de données
|
||||
*/
|
||||
@POST
|
||||
@Path("/database/optimize")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
|
||||
@Operation(summary = "Optimiser la base de données (VACUUM ANALYZE)")
|
||||
public Response optimizeDatabase() {
|
||||
log.info("POST /api/system/database/optimize");
|
||||
return Response.ok(systemConfigService.optimizeDatabase()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcer la déconnexion globale
|
||||
*/
|
||||
@POST
|
||||
@Path("/auth/logout-all")
|
||||
@RolesAllowed({"SUPER_ADMIN"})
|
||||
@Operation(summary = "Forcer la déconnexion de tous les utilisateurs")
|
||||
public Response forceGlobalLogout() {
|
||||
log.info("POST /api/system/auth/logout-all");
|
||||
return Response.ok(systemConfigService.forceGlobalLogout()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoyer les sessions expirées
|
||||
*/
|
||||
@POST
|
||||
@Path("/sessions/cleanup")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
|
||||
@Operation(summary = "Nettoyer les sessions expirées")
|
||||
public Response cleanupSessions() {
|
||||
log.info("POST /api/system/sessions/cleanup");
|
||||
return Response.ok(systemConfigService.cleanupSessions()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoyer les anciens logs
|
||||
*/
|
||||
@POST
|
||||
@Path("/logs/cleanup")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
|
||||
@Operation(summary = "Nettoyer les anciens logs selon la politique de rétention")
|
||||
public Response cleanOldLogs() {
|
||||
log.info("POST /api/system/logs/cleanup");
|
||||
return Response.ok(systemConfigService.cleanOldLogs()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Purger les données expirées
|
||||
*/
|
||||
@POST
|
||||
@Path("/data/purge")
|
||||
@RolesAllowed({"SUPER_ADMIN"})
|
||||
@Operation(summary = "Purger les données expirées (RGPD)")
|
||||
public Response purgeExpiredData() {
|
||||
log.info("POST /api/system/data/purge");
|
||||
return Response.ok(systemConfigService.purgeExpiredData()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyser les performances de la base de données
|
||||
*/
|
||||
@POST
|
||||
@Path("/performance/analyze")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
|
||||
@Operation(summary = "Analyser les performances du système")
|
||||
public Response analyzePerformance() {
|
||||
log.info("POST /api/system/performance/analyze");
|
||||
return Response.ok(systemConfigService.analyzePerformance()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer une sauvegarde
|
||||
*/
|
||||
@POST
|
||||
@Path("/backup/create")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
|
||||
@Operation(summary = "Créer une sauvegarde du système")
|
||||
public Response createBackup() {
|
||||
log.info("POST /api/system/backup/create");
|
||||
return Response.ok(systemConfigService.createBackup()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Planifier une maintenance
|
||||
*/
|
||||
@POST
|
||||
@Path("/maintenance/schedule")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
|
||||
@Operation(summary = "Planifier une maintenance")
|
||||
public Response scheduleMaintenance(@QueryParam("scheduledAt") String scheduledAt, @QueryParam("reason") String reason) {
|
||||
log.info("POST /api/system/maintenance/schedule");
|
||||
return Response.ok(systemConfigService.scheduleMaintenance(scheduledAt, reason)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activer la maintenance d'urgence
|
||||
*/
|
||||
@POST
|
||||
@Path("/maintenance/emergency")
|
||||
@RolesAllowed({"SUPER_ADMIN"})
|
||||
@Operation(summary = "Activer le mode maintenance d'urgence")
|
||||
public Response emergencyMaintenance() {
|
||||
log.info("POST /api/system/maintenance/emergency");
|
||||
return Response.ok(systemConfigService.emergencyMaintenance()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifier les mises à jour
|
||||
*/
|
||||
@GET
|
||||
@Path("/updates/check")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
|
||||
@Operation(summary = "Vérifier les mises à jour disponibles")
|
||||
public Response checkUpdates() {
|
||||
log.info("GET /api/system/updates/check");
|
||||
return Response.ok(systemConfigService.checkUpdates()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporter les logs récents
|
||||
*/
|
||||
@GET
|
||||
@Path("/logs/export")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
|
||||
@Operation(summary = "Exporter les logs des dernières 24h")
|
||||
public Response exportLogs() {
|
||||
log.info("GET /api/system/logs/export");
|
||||
return Response.ok(systemConfigService.exportLogs()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Générer un rapport d'utilisation
|
||||
*/
|
||||
@GET
|
||||
@Path("/reports/usage")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN"})
|
||||
@Operation(summary = "Générer un rapport d'utilisation du système")
|
||||
public Response generateUsageReport() {
|
||||
log.info("GET /api/system/reports/usage");
|
||||
return Response.ok(systemConfigService.generateUsageReport()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Générer un rapport d'audit
|
||||
*/
|
||||
@GET
|
||||
@Path("/audit/report")
|
||||
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MODERATEUR"})
|
||||
@Operation(summary = "Générer un rapport d'audit")
|
||||
public Response generateAuditReport() {
|
||||
log.info("GET /api/system/audit/report");
|
||||
return Response.ok(systemConfigService.generateAuditReport()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export RGPD
|
||||
*/
|
||||
@POST
|
||||
@Path("/gdpr/export")
|
||||
@RolesAllowed({"SUPER_ADMIN"})
|
||||
@Operation(summary = "Initier un export RGPD des données utilisateurs")
|
||||
public Response exportGDPRData() {
|
||||
log.info("POST /api/system/gdpr/export");
|
||||
return Response.ok(systemConfigService.exportGDPRData()).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.versement.request.DeclarerVersementManuelRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.request.InitierDepotEpargneRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.request.InitierVersementWaveRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementGatewayResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementStatutResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementSummaryResponse;
|
||||
import dev.lions.unionflow.server.service.VersementService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Resource REST pour la gestion des versements.
|
||||
*
|
||||
* <p>Endpoints principaux :
|
||||
* <ul>
|
||||
* <li>{@code POST /api/versements/initier-wave} — démarre le flux Wave deep link</li>
|
||||
* <li>{@code GET /api/versements/statut/{intentionId}} — retour deep link / polling</li>
|
||||
* <li>{@code POST /api/versements/declarer-manuel} — déclaration espèces/virement/chèque</li>
|
||||
* <li>{@code GET /api/versements/mes-versements} — historique du membre connecté</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@Path("/api/versements")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER"})
|
||||
@Tag(name = "Versements", description = "Versements de cotisations — Wave et manuel")
|
||||
public class VersementResource {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(VersementResource.class);
|
||||
|
||||
@Inject
|
||||
VersementService versementService;
|
||||
|
||||
// ── Lecture ───────────────────────────────────────────────────────────────
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
public Response trouverParId(@PathParam("id") UUID id) {
|
||||
LOG.infof("GET /api/versements/%s", id);
|
||||
VersementResponse result = versementService.trouverParId(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/reference/{numeroReference}")
|
||||
public Response trouverParNumeroReference(
|
||||
@PathParam("numeroReference") String numeroReference) {
|
||||
LOG.infof("GET /api/versements/reference/%s", numeroReference);
|
||||
VersementResponse result = versementService.trouverParNumeroReference(numeroReference);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/membre/{membreId}")
|
||||
public Response listerParMembre(@PathParam("membreId") UUID membreId) {
|
||||
LOG.infof("GET /api/versements/membre/%s", membreId);
|
||||
List<VersementSummaryResponse> result = versementService.listerParMembre(membreId);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/mes-versements")
|
||||
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION"})
|
||||
public Response getMesVersements(
|
||||
@QueryParam("limit") @DefaultValue("20") int limit) {
|
||||
LOG.infof("GET /api/versements/mes-versements?limit=%d", limit);
|
||||
List<VersementSummaryResponse> result = versementService.getMesVersements(limit);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
// ── Validation / Annulation ───────────────────────────────────────────────
|
||||
|
||||
@POST
|
||||
@Path("/{id}/valider")
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
|
||||
public Response validerVersement(@PathParam("id") UUID id) {
|
||||
LOG.infof("POST /api/versements/%s/valider", id);
|
||||
VersementResponse result = versementService.validerVersement(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{id}/annuler")
|
||||
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
|
||||
public Response annulerVersement(@PathParam("id") UUID id) {
|
||||
LOG.infof("POST /api/versements/%s/annuler", id);
|
||||
VersementResponse result = versementService.annulerVersement(id);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
// ── Flux Wave ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Initie un versement Wave.
|
||||
*
|
||||
* <p>Le mobile appelle cet endpoint puis ouvre le {@code waveLaunchUrl} retourné
|
||||
* avec {@code url_launcher}. Wave s'ouvre avec le montant et le numéro pré-remplis.
|
||||
* Après confirmation, Wave redirige vers
|
||||
* {@code unionflow://payment?result=success&ref={clientReference}}.
|
||||
*/
|
||||
@POST
|
||||
@Path("/initier-wave")
|
||||
@RolesAllowed({"MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER"})
|
||||
public Response initierVersementWave(@Valid InitierVersementWaveRequest request) {
|
||||
LOG.infof("POST /api/versements/initier-wave — cotisation: %s", request.cotisationId());
|
||||
VersementGatewayResponse result = versementService.initierVersementWave(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retour deep link / polling du statut d'un versement Wave.
|
||||
*
|
||||
* <p>Appelé par le mobile au retour du deep link
|
||||
* {@code unionflow://payment?result=success&ref={intentionId}}
|
||||
* pour confirmer que le paiement est bien enregistré côté UnionFlow.
|
||||
* Également utilisé par le web en polling toutes les 3 secondes.
|
||||
*/
|
||||
@GET
|
||||
@Path("/statut/{intentionId}")
|
||||
@RolesAllowed({"MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER"})
|
||||
public Response getStatutVersement(@PathParam("intentionId") UUID intentionId) {
|
||||
LOG.infof("GET /api/versements/statut/%s", intentionId);
|
||||
VersementStatutResponse result = versementService.verifierStatutVersement(intentionId);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
// ── Flux manuel ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Déclare un versement manuel (espèces, virement, chèque).
|
||||
* Le versement est créé avec le statut EN_ATTENTE_VALIDATION.
|
||||
* Le trésorier devra le valider via la page admin.
|
||||
*/
|
||||
@POST
|
||||
@Path("/declarer-manuel")
|
||||
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION"})
|
||||
public Response declarerVersementManuel(@Valid DeclarerVersementManuelRequest request) {
|
||||
LOG.infof("POST /api/versements/declarer-manuel — cotisation: %s, méthode: %s",
|
||||
request.cotisationId(), request.methodePaiement());
|
||||
VersementResponse result = versementService.declarerVersementManuel(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
|
||||
// ── Dépôt épargne ─────────────────────────────────────────────────────────
|
||||
|
||||
@POST
|
||||
@Path("/initier-depot-epargne")
|
||||
@RolesAllowed({"MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER"})
|
||||
public Response initierDepotEpargne(@Valid InitierDepotEpargneRequest request) {
|
||||
LOG.infof("POST /api/versements/initier-depot-epargne — compte: %s, montant: %s",
|
||||
request.compteId(), request.montant());
|
||||
VersementGatewayResponse result = versementService.initierDepotEpargneEnLigne(request);
|
||||
return Response.status(Response.Status.CREATED).entity(result).build();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneReq
|
||||
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.TypeTransactionEpargne;
|
||||
import dev.lions.unionflow.server.entity.IntentionPaiement;
|
||||
import dev.lions.unionflow.server.repository.IntentionPaiementRepository;
|
||||
import dev.lions.unionflow.server.service.PaiementService;
|
||||
import dev.lions.unionflow.server.service.VersementService;
|
||||
import dev.lions.unionflow.server.service.mutuelle.epargne.TransactionEpargneService;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.ws.rs.Produces;
|
||||
@@ -51,7 +51,7 @@ public class WaveRedirectResource {
|
||||
TransactionEpargneService transactionEpargneService;
|
||||
|
||||
@Inject
|
||||
PaiementService paiementService;
|
||||
VersementService versementService;
|
||||
|
||||
@GET
|
||||
@Path("/success")
|
||||
@@ -172,8 +172,8 @@ public class WaveRedirectResource {
|
||||
}
|
||||
}
|
||||
|
||||
// Déléguer la complétion cotisations au service
|
||||
paiementService.completerIntention(intention, null);
|
||||
// Déléguer la confirmation cotisations au service
|
||||
versementService.confirmerVersementWave(intention, null);
|
||||
LOG.infof("Wave: intention %s complétée", ref);
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Wave: erreur applyCompletion ref=%s", ref);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.lions.unionflow.server.security;
|
||||
|
||||
import dev.lions.unionflow.server.service.OrganisationModuleService;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Priorities;
|
||||
@@ -44,6 +45,9 @@ public class ModuleAccessFilter implements ContainerRequestFilter {
|
||||
@Inject
|
||||
OrganisationModuleService organisationModuleService;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity identity;
|
||||
|
||||
@Context
|
||||
ResourceInfo resourceInfo;
|
||||
|
||||
@@ -61,6 +65,11 @@ public class ModuleAccessFilter implements ContainerRequestFilter {
|
||||
return;
|
||||
}
|
||||
|
||||
// SUPER_ADMIN a accès global à tous les modules sans contexte d'organisation
|
||||
if (identity.hasRole("SUPER_ADMIN")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String moduleRequis = annotation.value().toUpperCase();
|
||||
|
||||
// 2. Extraire l'organisation active depuis le header
|
||||
|
||||
@@ -52,21 +52,11 @@ public class AdminUserService {
|
||||
}
|
||||
|
||||
public List<RoleDTO> getRealmRoles() {
|
||||
try {
|
||||
return roleServiceClient.getRealmRoles(DEFAULT_REALM);
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Impossible de récupérer les rôles realm: %s", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
return roleServiceClient.getRealmRoles(DEFAULT_REALM);
|
||||
}
|
||||
|
||||
public List<RoleDTO> getUserRoles(String userId) {
|
||||
try {
|
||||
return roleServiceClient.getUserRealmRoles(userId, DEFAULT_REALM);
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Impossible de récupérer les rôles de l'utilisateur %s: %s", userId, e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
return roleServiceClient.getUserRealmRoles(userId, DEFAULT_REALM);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse;
|
||||
import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
|
||||
import dev.lions.unionflow.server.entity.Conversation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Message;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.ConversationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.MessageRepository;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service de gestion des conversations
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-16
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class ConversationService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ConversationService.class);
|
||||
|
||||
@Inject
|
||||
ConversationRepository conversationRepository;
|
||||
|
||||
@Inject
|
||||
MessageRepository messageRepository;
|
||||
|
||||
@Inject
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@Inject
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
/**
|
||||
* Liste les conversations d'un membre
|
||||
*/
|
||||
public List<ConversationResponse> getConversations(UUID membreId, UUID organisationId, boolean includeArchived) {
|
||||
LOG.infof("Récupération conversations pour membre %s", membreId);
|
||||
|
||||
List<Conversation> conversations;
|
||||
if (organisationId != null) {
|
||||
conversations = conversationRepository.findByOrganisation(organisationId);
|
||||
} else {
|
||||
conversations = conversationRepository.findByParticipant(membreId, includeArchived);
|
||||
}
|
||||
|
||||
return conversations.stream()
|
||||
.map(c -> convertToResponse(c, membreId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une conversation par ID
|
||||
*/
|
||||
public ConversationResponse getConversationById(UUID conversationId, UUID membreId) {
|
||||
Conversation conversation = conversationRepository.findByIdAndParticipant(conversationId, membreId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée ou accès refusé"));
|
||||
|
||||
return convertToResponse(conversation, membreId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle conversation
|
||||
*/
|
||||
@Transactional
|
||||
public ConversationResponse createConversation(CreateConversationRequest request, UUID creatorId) {
|
||||
LOG.infof("Création conversation: %s (type: %s)", request.name(), request.type());
|
||||
|
||||
Conversation conversation = new Conversation();
|
||||
conversation.setName(request.name());
|
||||
conversation.setDescription(request.description());
|
||||
conversation.setType(request.type());
|
||||
|
||||
// Ajouter les participants
|
||||
List<Membre> participants = request.participantIds().stream()
|
||||
.map(id -> membreRepository.findById(id))
|
||||
.filter(membre -> membre != null)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Ajouter le créateur s'il n'est pas dans la liste
|
||||
Membre creator = membreRepository.findById(creatorId);
|
||||
if (creator != null && !participants.contains(creator)) {
|
||||
participants.add(creator);
|
||||
}
|
||||
|
||||
conversation.setParticipants(participants);
|
||||
|
||||
// Organisation
|
||||
if (request.organisationId() != null) {
|
||||
Organisation org = organisationRepository.findById(request.organisationId());
|
||||
conversation.setOrganisation(org);
|
||||
}
|
||||
|
||||
conversation.setUpdatedAt(LocalDateTime.now());
|
||||
conversationRepository.persist(conversation);
|
||||
|
||||
return convertToResponse(conversation, creatorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive/désarchive une conversation
|
||||
*/
|
||||
@Transactional
|
||||
public void archiveConversation(UUID conversationId, UUID membreId, boolean archive) {
|
||||
Conversation conversation = conversationRepository.findByIdAndParticipant(conversationId, membreId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée"));
|
||||
|
||||
conversation.setIsArchived(archive);
|
||||
conversation.setUpdatedAt(LocalDateTime.now());
|
||||
conversationRepository.persist(conversation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque une conversation comme lue
|
||||
*/
|
||||
@Transactional
|
||||
public void markAsRead(UUID conversationId, UUID membreId) {
|
||||
// Vérifier accès
|
||||
conversationRepository.findByIdAndParticipant(conversationId, membreId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée"));
|
||||
|
||||
messageRepository.markAllAsReadByConversationAndMember(conversationId, membreId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle mute
|
||||
*/
|
||||
@Transactional
|
||||
public void toggleMute(UUID conversationId, UUID membreId) {
|
||||
Conversation conversation = conversationRepository.findByIdAndParticipant(conversationId, membreId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée"));
|
||||
|
||||
conversation.setIsMuted(!conversation.getIsMuted());
|
||||
conversation.setUpdatedAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle pin
|
||||
*/
|
||||
@Transactional
|
||||
public void togglePin(UUID conversationId, UUID membreId) {
|
||||
Conversation conversation = conversationRepository.findByIdAndParticipant(conversationId, membreId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée"));
|
||||
|
||||
conversation.setIsPinned(!conversation.getIsPinned());
|
||||
conversation.setUpdatedAt(LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit Conversation en DTO
|
||||
*/
|
||||
private ConversationResponse convertToResponse(Conversation c, UUID currentUserId) {
|
||||
Message lastMsg = messageRepository.findLastByConversation(c.getId());
|
||||
long unreadCount = messageRepository.countUnreadByConversationAndMember(c.getId(), currentUserId);
|
||||
|
||||
return ConversationResponse.builder()
|
||||
.id(c.getId())
|
||||
.name(c.getName())
|
||||
.description(c.getDescription())
|
||||
.type(c.getType())
|
||||
.participantIds(c.getParticipants().stream().map(Membre::getId).collect(Collectors.toList()))
|
||||
.organisationId(c.getOrganisation() != null ? c.getOrganisation().getId() : null)
|
||||
.lastMessage(lastMsg != null ? convertMessageToResponse(lastMsg) : null)
|
||||
.unreadCount((int) unreadCount)
|
||||
.muted(Boolean.TRUE.equals(c.getIsMuted()))
|
||||
.pinned(Boolean.TRUE.equals(c.getIsPinned()))
|
||||
.archived(Boolean.TRUE.equals(c.getIsArchived()))
|
||||
.createdAt(c.getDateCreation())
|
||||
.updatedAt(c.getUpdatedAt())
|
||||
.avatarUrl(c.getAvatarUrl())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit Message en DTO simple
|
||||
*/
|
||||
private MessageResponse convertMessageToResponse(Message m) {
|
||||
return MessageResponse.builder()
|
||||
.id(m.getId())
|
||||
.conversationId(m.getConversation().getId())
|
||||
.senderId(m.getSender().getId())
|
||||
.senderName(m.getSenderName())
|
||||
.senderAvatar(m.getSenderAvatar())
|
||||
.content(m.getContent())
|
||||
.type(m.getType())
|
||||
.status(m.getStatus())
|
||||
.priority(m.getPriority())
|
||||
.createdAt(m.getDateCreation())
|
||||
.edited(Boolean.TRUE.equals(m.getIsEdited()))
|
||||
.deleted(Boolean.TRUE.equals(m.getIsDeleted()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import io.quarkus.panache.common.Sort;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -63,6 +64,7 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRED)
|
||||
public DashboardDataResponse getDashboardData(String organizationId, String userId) {
|
||||
LOG.infof("Récupération des données dashboard pour org: %s et user: %s", organizationId, userId);
|
||||
|
||||
@@ -77,6 +79,7 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRED)
|
||||
public DashboardStatsResponse getDashboardStats(String organizationId, String userId) {
|
||||
LOG.infof("Récupération des stats dashboard pour org: %s et user: %s", organizationId, userId);
|
||||
|
||||
@@ -171,6 +174,7 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRED)
|
||||
public List<RecentActivityResponse> getRecentActivities(String organizationId, String userId, int limit) {
|
||||
LOG.infof("Récupération de %d activités récentes pour org: %s et user: %s", limit, organizationId, userId);
|
||||
|
||||
@@ -253,6 +257,7 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRED)
|
||||
public List<UpcomingEventResponse> getUpcomingEvents(String organizationId, String userId, int limit) {
|
||||
LOG.infof("Récupération de %d événements à venir pour org: %s et user: %s", limit, organizationId, userId);
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Client HTTP direct vers l'API Admin Keycloak (sans JAX-RS pour éviter les conflits ObjectMapper).
|
||||
* Pattern identique à KeycloakAdminClientImpl.getAllRealms() — HttpClient Java 11 + ObjectMapper isolé.
|
||||
*/
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
public class KeycloakAdminHttpClient {
|
||||
|
||||
@ConfigProperty(name = "keycloak.admin.url", defaultValue = "http://localhost:8180")
|
||||
String keycloakUrl;
|
||||
|
||||
@ConfigProperty(name = "keycloak.admin.username", defaultValue = "admin")
|
||||
String adminUsername;
|
||||
|
||||
@ConfigProperty(name = "keycloak.admin.password", defaultValue = "admin")
|
||||
String adminPassword;
|
||||
|
||||
@ConfigProperty(name = "keycloak.admin.realm", defaultValue = "unionflow")
|
||||
String realm;
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
/**
|
||||
* Obtenir un token admin depuis le realm master via client_credentials admin-cli
|
||||
*/
|
||||
private String getAdminToken() throws Exception {
|
||||
String body = "client_id=admin-cli"
|
||||
+ "&username=" + java.net.URLEncoder.encode(adminUsername, java.nio.charset.StandardCharsets.UTF_8)
|
||||
+ "&password=" + java.net.URLEncoder.encode(adminPassword, java.nio.charset.StandardCharsets.UTF_8)
|
||||
+ "&grant_type=password";
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(keycloakUrl + "/realms/master/protocol/openid-connect/token"))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("Échec authentification admin Keycloak (HTTP " + response.statusCode() + "): " + response.body());
|
||||
}
|
||||
|
||||
return mapper.readTree(response.body()).get("access_token").asText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Révoquer toutes les sessions actives du realm.
|
||||
* Stratégie : lister tous les users → POST /users/{id}/logout pour chaque.
|
||||
* @return nombre de sessions révoquées
|
||||
*/
|
||||
public int logoutAllSessions() throws Exception {
|
||||
String token = getAdminToken();
|
||||
log.info("Token admin Keycloak obtenu — déconnexion de toutes les sessions du realm '{}'", realm);
|
||||
|
||||
// Récupérer tous les utilisateurs (max 1000)
|
||||
HttpRequest usersRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(keycloakUrl + "/admin/realms/" + realm + "/users?max=1000&enabled=true"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.GET()
|
||||
.timeout(Duration.ofSeconds(15))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> usersResponse = httpClient.send(usersRequest, HttpResponse.BodyHandlers.ofString());
|
||||
if (usersResponse.statusCode() != 200) {
|
||||
throw new RuntimeException("Impossible de lister les utilisateurs Keycloak (HTTP " + usersResponse.statusCode() + ")");
|
||||
}
|
||||
|
||||
var users = mapper.readTree(usersResponse.body());
|
||||
int loggedOut = 0;
|
||||
|
||||
for (var user : users) {
|
||||
String userId = user.get("id").asText();
|
||||
String username = user.has("username") ? user.get("username").asText() : userId;
|
||||
|
||||
HttpRequest logoutRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(keycloakUrl + "/admin/realms/" + realm + "/users/" + userId + "/logout"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> logoutResponse = httpClient.send(logoutRequest, HttpResponse.BodyHandlers.ofString());
|
||||
if (logoutResponse.statusCode() == 204 || logoutResponse.statusCode() == 200) {
|
||||
loggedOut++;
|
||||
log.debug("Session révoquée pour l'utilisateur '{}'", username);
|
||||
} else {
|
||||
log.warn("Impossible de révoquer la session de '{}' (HTTP {})", username, logoutResponse.statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Déconnexion globale terminée: {}/{} utilisateur(s) déconnecté(s)", loggedOut, users.size());
|
||||
return loggedOut;
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest;
|
||||
import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
|
||||
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 dev.lions.unionflow.server.entity.Conversation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Message;
|
||||
import dev.lions.unionflow.server.repository.ConversationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.MessageRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service de gestion des messages
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-16
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class MessageService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MessageService.class);
|
||||
|
||||
@Inject
|
||||
MessageRepository messageRepository;
|
||||
|
||||
@Inject
|
||||
ConversationRepository conversationRepository;
|
||||
|
||||
@Inject
|
||||
MembreRepository membreRepository;
|
||||
|
||||
/**
|
||||
* Récupère les messages d'une conversation
|
||||
*/
|
||||
public List<MessageResponse> getMessages(UUID conversationId, UUID membreId, int limit) {
|
||||
LOG.infof("Récupération messages pour conversation %s (limit: %d)", conversationId, limit);
|
||||
|
||||
// Vérifier accès
|
||||
conversationRepository.findByIdAndParticipant(conversationId, membreId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée ou accès refusé"));
|
||||
|
||||
List<Message> messages = messageRepository.findByConversation(conversationId, limit);
|
||||
|
||||
return messages.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un message
|
||||
*/
|
||||
@Transactional
|
||||
public MessageResponse sendMessage(SendMessageRequest request, UUID senderId) {
|
||||
LOG.infof("Envoi message dans conversation %s", request.conversationId());
|
||||
|
||||
// Vérifier accès conversation
|
||||
Conversation conversation = conversationRepository.findByIdAndParticipant(request.conversationId(), senderId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée ou accès refusé"));
|
||||
|
||||
Membre sender = membreRepository.findById(senderId);
|
||||
if (sender == null) {
|
||||
throw new NotFoundException("Expéditeur non trouvé");
|
||||
}
|
||||
|
||||
Message message = new Message();
|
||||
message.setConversation(conversation);
|
||||
message.setSender(sender);
|
||||
message.setSenderName(sender.getPrenom() + " " + sender.getNom());
|
||||
message.setSenderAvatar(sender.getPhotoUrl());
|
||||
message.setContent(request.content());
|
||||
message.setType(request.type() != null ? request.type() : MessageType.INDIVIDUAL);
|
||||
message.setStatus(MessageStatus.SENT);
|
||||
message.setPriority(request.priority() != null ? request.priority() : MessagePriority.NORMAL);
|
||||
|
||||
// Destinataires (pour targeted messages)
|
||||
if (request.recipientIds() != null && !request.recipientIds().isEmpty()) {
|
||||
message.setRecipientIds(request.recipientIds().stream()
|
||||
.map(UUID::toString)
|
||||
.collect(Collectors.joining(",")));
|
||||
}
|
||||
|
||||
// Rôles destinataires
|
||||
if (request.recipientRoles() != null && !request.recipientRoles().isEmpty()) {
|
||||
message.setRecipientRoles(String.join(",", request.recipientRoles()));
|
||||
}
|
||||
|
||||
// Pièces jointes
|
||||
if (request.attachments() != null && !request.attachments().isEmpty()) {
|
||||
message.setAttachments(String.join(",", request.attachments()));
|
||||
}
|
||||
|
||||
message.setOrganisation(conversation.getOrganisation());
|
||||
|
||||
messageRepository.persist(message);
|
||||
|
||||
// Mettre à jour la conversation
|
||||
conversation.setUpdatedAt(LocalDateTime.now());
|
||||
conversationRepository.persist(conversation);
|
||||
|
||||
LOG.infof("Message %s créé avec succès", message.getId());
|
||||
return convertToResponse(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Édite un message
|
||||
*/
|
||||
@Transactional
|
||||
public MessageResponse editMessage(UUID messageId, UUID senderId, String newContent) {
|
||||
Message message = messageRepository.findById(messageId);
|
||||
if (message == null) {
|
||||
throw new NotFoundException("Message non trouvé");
|
||||
}
|
||||
|
||||
if (!message.getSender().getId().equals(senderId)) {
|
||||
throw new IllegalStateException("Vous ne pouvez éditer que vos propres messages");
|
||||
}
|
||||
|
||||
message.setContent(newContent);
|
||||
message.markAsEdited();
|
||||
messageRepository.persist(message);
|
||||
|
||||
return convertToResponse(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un message (soft delete)
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteMessage(UUID messageId, UUID senderId) {
|
||||
Message message = messageRepository.findById(messageId);
|
||||
if (message == null) {
|
||||
throw new NotFoundException("Message non trouvé");
|
||||
}
|
||||
|
||||
if (!message.getSender().getId().equals(senderId)) {
|
||||
throw new IllegalStateException("Vous ne pouvez supprimer que vos propres messages");
|
||||
}
|
||||
|
||||
message.setIsDeleted(true);
|
||||
message.setContent("[Message supprimé]");
|
||||
messageRepository.persist(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit Message en DTO
|
||||
*/
|
||||
private MessageResponse convertToResponse(Message m) {
|
||||
// Parser recipient IDs
|
||||
List<UUID> recipientIds = null;
|
||||
if (m.getRecipientIds() != null && !m.getRecipientIds().isEmpty()) {
|
||||
recipientIds = List.of(m.getRecipientIds().split(",")).stream()
|
||||
.map(UUID::fromString)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Parser roles
|
||||
List<String> roles = null;
|
||||
if (m.getRecipientRoles() != null && !m.getRecipientRoles().isEmpty()) {
|
||||
roles = List.of(m.getRecipientRoles().split(","));
|
||||
}
|
||||
|
||||
// Parser attachments
|
||||
List<String> attachments = null;
|
||||
if (m.getAttachments() != null && !m.getAttachments().isEmpty()) {
|
||||
attachments = List.of(m.getAttachments().split(","));
|
||||
}
|
||||
|
||||
return MessageResponse.builder()
|
||||
.id(m.getId())
|
||||
.conversationId(m.getConversation().getId())
|
||||
.senderId(m.getSender().getId())
|
||||
.senderName(m.getSenderName())
|
||||
.senderAvatar(m.getSenderAvatar())
|
||||
.content(m.getContent())
|
||||
.type(m.getType())
|
||||
.status(m.getStatus())
|
||||
.priority(m.getPriority())
|
||||
.recipientIds(recipientIds)
|
||||
.recipientRoles(roles)
|
||||
.organisationId(m.getOrganisation() != null ? m.getOrganisation().getId() : null)
|
||||
.createdAt(m.getDateCreation())
|
||||
.readAt(m.getReadAt())
|
||||
.attachments(attachments)
|
||||
.edited(Boolean.TRUE.equals(m.getIsEdited()))
|
||||
.editedAt(m.getEditedAt())
|
||||
.deleted(Boolean.TRUE.equals(m.getIsDeleted()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.BloquerMembreRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationDirecteRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationRoleRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.EnvoyerMessageRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.MettreAJourPolitiqueRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication;
|
||||
import dev.lions.unionflow.server.entity.ContactPolicy;
|
||||
import dev.lions.unionflow.server.entity.Conversation;
|
||||
import dev.lions.unionflow.server.entity.ConversationParticipant;
|
||||
import dev.lions.unionflow.server.entity.MemberBlock;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.MembreOrganisation;
|
||||
import dev.lions.unionflow.server.entity.Message;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.ContactPolicyRepository;
|
||||
import dev.lions.unionflow.server.repository.ConversationParticipantRepository;
|
||||
import dev.lions.unionflow.server.repository.ConversationRepository;
|
||||
import dev.lions.unionflow.server.repository.MemberBlockRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.MessageRepository;
|
||||
import dev.lions.unionflow.server.messaging.KafkaEventProducer;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.ForbiddenException;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la messagerie instantanée.
|
||||
*
|
||||
* <p>Gère les conversations (directes et canaux-rôle), les messages (texte,
|
||||
* vocal, image), les blocages et les politiques de communication.
|
||||
*
|
||||
* <p>Politique par appartenance : deux membres de la même organisation
|
||||
* peuvent se contacter sans demande d'amitié préalable.
|
||||
* L'adhésion est la relation de confiance.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class MessagingService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(MessagingService.class);
|
||||
private static final int PAGE_SIZE_DEFAULT = 30;
|
||||
|
||||
@Inject ConversationRepository conversationRepository;
|
||||
@Inject ConversationParticipantRepository participantRepository;
|
||||
@Inject MessageRepository messageRepository;
|
||||
@Inject ContactPolicyRepository contactPolicyRepository;
|
||||
@Inject MemberBlockRepository memberBlockRepository;
|
||||
@Inject MembreRepository membreRepository;
|
||||
@Inject MembreOrganisationRepository membreOrganisationRepository;
|
||||
@Inject KafkaEventProducer kafkaEventProducer;
|
||||
@Inject io.quarkus.security.identity.SecurityIdentity securityIdentity;
|
||||
|
||||
// ── Conversations ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Démarre ou récupère une conversation directe 1-1.
|
||||
* Idempotent : si la conversation existe déjà, elle est retournée.
|
||||
*/
|
||||
@Transactional
|
||||
public ConversationResponse demarrerConversationDirecte(DemarrerConversationDirecteRequest request) {
|
||||
Membre moi = getMembreConnecte();
|
||||
Membre destinataire = membreRepository.findById(request.destinataireId());
|
||||
if (destinataire == null) {
|
||||
throw new NotFoundException("Membre destinataire non trouvé : " + request.destinataireId());
|
||||
}
|
||||
|
||||
UUID orgId = request.organisationId();
|
||||
verifierAppartenance(moi.getId(), orgId);
|
||||
verifierAppartenance(request.destinataireId(), orgId);
|
||||
verifierPolitique(moi.getId(), request.destinataireId(), orgId, false);
|
||||
|
||||
// Idempotence : chercher une conversation directe existante
|
||||
return conversationRepository
|
||||
.findConversationDirecte(moi.getId(), request.destinataireId(), orgId)
|
||||
.map(c -> {
|
||||
// Envoyer le message initial si fourni
|
||||
if (request.contenuInitial() != null && !request.contenuInitial().isBlank()) {
|
||||
envoyerMessageDansConversation(c, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
|
||||
}
|
||||
return toConversationResponse(c, moi.getId());
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
Organisation org = getOrganisation(orgId);
|
||||
Conversation conv = Conversation.builder()
|
||||
.organisation(org)
|
||||
.typeConversation(TypeConversation.DIRECTE)
|
||||
.statut(StatutConversation.ACTIVE)
|
||||
.build();
|
||||
conversationRepository.persist(conv);
|
||||
|
||||
ajouterParticipant(conv, moi, "INITIATEUR");
|
||||
ajouterParticipant(conv, destinataire, "PARTICIPANT");
|
||||
|
||||
if (request.contenuInitial() != null && !request.contenuInitial().isBlank()) {
|
||||
envoyerMessageDansConversation(conv, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
|
||||
}
|
||||
|
||||
LOG.infof("Conversation directe créée: %s ↔ %s dans org %s",
|
||||
moi.getEmail(), destinataire.getEmail(), orgId);
|
||||
return toConversationResponse(conv, moi.getId());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarre ou récupère un canal de rôle.
|
||||
* Le canal est partagé : tous les membres qui contactent "Le Trésorier"
|
||||
* aboutissent dans le même canal.
|
||||
*/
|
||||
@Transactional
|
||||
public ConversationResponse demarrerConversationRole(DemarrerConversationRoleRequest request) {
|
||||
Membre moi = getMembreConnecte();
|
||||
UUID orgId = request.organisationId();
|
||||
verifierAppartenance(moi.getId(), orgId);
|
||||
verifierPolitique(moi.getId(), null, orgId, true);
|
||||
|
||||
String roleCible = request.roleCible();
|
||||
List<Membre> porteurs = trouverPorteursDuRole(orgId, roleCible);
|
||||
if (porteurs.isEmpty()) {
|
||||
throw new NotFoundException("Aucun membre avec le rôle " + roleCible + " dans cette organisation");
|
||||
}
|
||||
|
||||
Organisation org = getOrganisation(orgId);
|
||||
String titreCanal = libelleDuRole(roleCible);
|
||||
|
||||
return conversationRepository.findCanalRole(orgId, roleCible)
|
||||
.map(c -> {
|
||||
// Ajouter l'initiateur s'il n'est pas encore participant
|
||||
if (!participantRepository.estParticipant(c.getId(), moi.getId())) {
|
||||
ajouterParticipant(c, moi, "PARTICIPANT");
|
||||
}
|
||||
envoyerMessageDansConversation(c, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
|
||||
return toConversationResponse(c, moi.getId());
|
||||
})
|
||||
.orElseGet(() -> {
|
||||
Conversation conv = Conversation.builder()
|
||||
.organisation(org)
|
||||
.typeConversation(TypeConversation.ROLE_CANAL)
|
||||
.roleCible(roleCible)
|
||||
.titre(titreCanal)
|
||||
.statut(StatutConversation.ACTIVE)
|
||||
.build();
|
||||
conversationRepository.persist(conv);
|
||||
|
||||
ajouterParticipant(conv, moi, "INITIATEUR");
|
||||
porteurs.forEach(p -> ajouterParticipant(conv, p, "MODERATEUR"));
|
||||
|
||||
envoyerMessageDansConversation(conv, moi, request.contenuInitial(), TypeContenu.TEXTE, null, null, null);
|
||||
|
||||
LOG.infof("Canal rôle créé: %s dans org %s", roleCible, orgId);
|
||||
return toConversationResponse(conv, moi.getId());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste des conversations du membre connecté.
|
||||
*/
|
||||
public List<ConversationSummaryResponse> getMesConversations() {
|
||||
Membre moi = getMembreConnecte();
|
||||
return conversationRepository.findByMembreId(moi.getId()).stream()
|
||||
.map(c -> toConversationSummary(c, moi.getId()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le détail d'une conversation (avec les derniers messages).
|
||||
*/
|
||||
public ConversationResponse getConversation(UUID conversationId) {
|
||||
Membre moi = getMembreConnecte();
|
||||
Conversation conv = conversationRepository.findConversationById(conversationId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
|
||||
verifierParticipant(conv, moi.getId());
|
||||
return toConversationResponse(conv, moi.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive une conversation.
|
||||
*/
|
||||
@Transactional
|
||||
public ConversationResponse archiverConversation(UUID conversationId) {
|
||||
Membre moi = getMembreConnecte();
|
||||
Conversation conv = conversationRepository.findConversationById(conversationId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
|
||||
verifierParticipant(conv, moi.getId());
|
||||
conv.archiver();
|
||||
return toConversationResponse(conv, moi.getId());
|
||||
}
|
||||
|
||||
// ── Messages ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Envoie un message dans une conversation existante.
|
||||
*/
|
||||
@Transactional
|
||||
public MessageResponse envoyerMessage(UUID conversationId, EnvoyerMessageRequest request) {
|
||||
Membre moi = getMembreConnecte();
|
||||
Conversation conv = conversationRepository.findConversationById(conversationId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
|
||||
|
||||
verifierParticipant(conv, moi.getId());
|
||||
if (!conv.estActive()) {
|
||||
throw new BadRequestException("Cette conversation est archivée");
|
||||
}
|
||||
|
||||
TypeContenu type = parseTypeContenu(request.typeMessage());
|
||||
validerContenuMessage(type, request);
|
||||
|
||||
Message message = envoyerMessageDansConversation(
|
||||
conv, moi,
|
||||
request.contenu(), type,
|
||||
request.urlFichier(), request.dureeAudio(),
|
||||
request.messageParentId()
|
||||
);
|
||||
return toMessageResponse(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les messages d'une conversation (paginés).
|
||||
*/
|
||||
public List<MessageResponse> getMessages(UUID conversationId, int page) {
|
||||
Membre moi = getMembreConnecte();
|
||||
Conversation conv = conversationRepository.findConversationById(conversationId)
|
||||
.orElseThrow(() -> new NotFoundException("Conversation non trouvée : " + conversationId));
|
||||
verifierParticipant(conv, moi.getId());
|
||||
|
||||
return messageRepository.findByConversationPagine(conversationId, page, PAGE_SIZE_DEFAULT)
|
||||
.stream()
|
||||
.map(this::toMessageResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque tous les messages d'une conversation comme lus.
|
||||
*/
|
||||
@Transactional
|
||||
public void marquerConversationLue(UUID conversationId) {
|
||||
Membre moi = getMembreConnecte();
|
||||
participantRepository.findParticipant(conversationId, moi.getId())
|
||||
.ifPresent(p -> {
|
||||
p.marquerLu();
|
||||
LOG.debugf("Conversation %s marquée lue par %s", conversationId, moi.getEmail());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un message (soft delete — contenu remplacé par "[Message supprimé]").
|
||||
*/
|
||||
@Transactional
|
||||
public void supprimerMessage(UUID conversationId, UUID messageId) {
|
||||
Membre moi = getMembreConnecte();
|
||||
Message message = messageRepository.findMessageById(messageId)
|
||||
.orElseThrow(() -> new NotFoundException("Message non trouvé : " + messageId));
|
||||
|
||||
if (!message.getConversation().getId().equals(conversationId)) {
|
||||
throw new NotFoundException("Message non trouvé dans cette conversation");
|
||||
}
|
||||
if (!message.getExpediteur().getId().equals(moi.getId())) {
|
||||
throw new ForbiddenException("Vous ne pouvez supprimer que vos propres messages");
|
||||
}
|
||||
message.supprimer();
|
||||
}
|
||||
|
||||
// ── Blocages ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Bloque un membre dans une organisation.
|
||||
*/
|
||||
@Transactional
|
||||
public void bloquerMembre(BloquerMembreRequest request) {
|
||||
Membre moi = getMembreConnecte();
|
||||
UUID membreABloquerId = request.membreABloquerId();
|
||||
UUID orgId = request.organisationId();
|
||||
|
||||
if (moi.getId().equals(membreABloquerId)) {
|
||||
throw new BadRequestException("Vous ne pouvez pas vous bloquer vous-même");
|
||||
}
|
||||
|
||||
Membre aBloquer = membreRepository.findById(membreABloquerId);
|
||||
if (aBloquer == null) {
|
||||
throw new NotFoundException("Membre non trouvé : " + membreABloquerId);
|
||||
}
|
||||
|
||||
if (memberBlockRepository.estBloque(moi.getId(), membreABloquerId, orgId)) {
|
||||
throw new BadRequestException("Ce membre est déjà bloqué");
|
||||
}
|
||||
|
||||
Organisation org = getOrganisation(orgId);
|
||||
MemberBlock block = MemberBlock.builder()
|
||||
.bloqueur(moi)
|
||||
.bloque(aBloquer)
|
||||
.organisation(org)
|
||||
.build();
|
||||
memberBlockRepository.persist(block);
|
||||
LOG.infof("%s a bloqué %s dans org %s", moi.getEmail(), aBloquer.getEmail(), orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Débloque un membre dans une organisation.
|
||||
*/
|
||||
@Transactional
|
||||
public void debloquerMembre(UUID membreId, UUID organisationId) {
|
||||
Membre moi = getMembreConnecte();
|
||||
MemberBlock block = memberBlockRepository.findBlocage(moi.getId(), membreId, organisationId)
|
||||
.orElseThrow(() -> new NotFoundException("Aucun blocage trouvé pour ce membre"));
|
||||
block.setActif(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste des membres bloqués par le membre connecté.
|
||||
*/
|
||||
public List<MemberBlock> getMesBlocages() {
|
||||
Membre moi = getMembreConnecte();
|
||||
return memberBlockRepository.findByBloqueur(moi.getId());
|
||||
}
|
||||
|
||||
// ── Politique de communication ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Retourne la politique de communication d'une organisation.
|
||||
* Crée une politique par défaut si elle n'existe pas encore.
|
||||
*/
|
||||
@Transactional
|
||||
public ContactPolicyResponse getPolitique(UUID organisationId) {
|
||||
ContactPolicy policy = contactPolicyRepository.findByOrganisationId(organisationId)
|
||||
.orElseGet(() -> creerPolitiqueParDefaut(organisationId));
|
||||
return toContactPolicyResponse(policy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la politique de communication.
|
||||
* Réservé aux ADMIN et ADMIN_ORGANISATION.
|
||||
*/
|
||||
@Transactional
|
||||
public ContactPolicyResponse mettreAJourPolitique(UUID organisationId, MettreAJourPolitiqueRequest request) {
|
||||
ContactPolicy policy = contactPolicyRepository.findByOrganisationId(organisationId)
|
||||
.orElseGet(() -> creerPolitiqueParDefaut(organisationId));
|
||||
|
||||
if (request.typePolitique() != null) {
|
||||
policy.setTypePolitique(TypePolitiqueCommunication.valueOf(request.typePolitique()));
|
||||
}
|
||||
if (request.autoriserMembreVersMembre() != null) {
|
||||
policy.setAutoriserMembreVersMembre(request.autoriserMembreVersMembre());
|
||||
}
|
||||
if (request.autoriserMembreVersRole() != null) {
|
||||
policy.setAutoriserMembreVersRole(request.autoriserMembreVersRole());
|
||||
}
|
||||
if (request.autoriserNotesVocales() != null) {
|
||||
policy.setAutoriserNotesVocales(request.autoriserNotesVocales());
|
||||
}
|
||||
return toContactPolicyResponse(policy);
|
||||
}
|
||||
|
||||
// ── Méthodes privées ──────────────────────────────────────────────────────
|
||||
|
||||
private Message envoyerMessageDansConversation(
|
||||
Conversation conv, Membre expediteur,
|
||||
String contenu, TypeContenu type,
|
||||
String urlFichier, Integer dureeAudio,
|
||||
UUID messageParentId) {
|
||||
|
||||
Message.MessageBuilder builder = Message.builder()
|
||||
.conversation(conv)
|
||||
.expediteur(expediteur)
|
||||
.typeMessage(type)
|
||||
.contenu(contenu)
|
||||
.urlFichier(urlFichier)
|
||||
.dureeAudio(dureeAudio);
|
||||
|
||||
if (messageParentId != null) {
|
||||
messageRepository.findMessageById(messageParentId)
|
||||
.ifPresent(builder::messageParent);
|
||||
}
|
||||
|
||||
Message message = builder.build();
|
||||
messageRepository.persist(message);
|
||||
conv.enregistrerNouveauMessage();
|
||||
|
||||
// Notifier via Kafka → WebSocket
|
||||
try {
|
||||
java.util.Map<String, Object> data = new java.util.HashMap<>();
|
||||
data.put("conversationId", conv.getId().toString());
|
||||
data.put("messageId", message.getId() != null ? message.getId().toString() : "");
|
||||
data.put("expediteurId", expediteur.getId().toString());
|
||||
data.put("typeMessage", type.name());
|
||||
kafkaEventProducer.publishNouveauMessage(conv.getId(), conv.getOrganisation().getId().toString(), data);
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Impossible de publier l'event Kafka pour le message: %s", e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private void ajouterParticipant(Conversation conv, Membre membre, String role) {
|
||||
if (!participantRepository.estParticipant(conv.getId(), membre.getId())) {
|
||||
ConversationParticipant participant = ConversationParticipant.builder()
|
||||
.conversation(conv)
|
||||
.membre(membre)
|
||||
.roleDansConversation(role)
|
||||
.notifier(true)
|
||||
.build();
|
||||
participantRepository.persist(participant);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifierAppartenance(UUID membreId, UUID organisationId) {
|
||||
boolean appartient = membreOrganisationRepository
|
||||
.count("membre.id = ?1 AND organisation.id = ?2 AND actif = true", membreId, organisationId) > 0;
|
||||
if (!appartient) {
|
||||
throw new ForbiddenException("Le membre n'appartient pas à cette organisation");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifierPolitique(UUID expediteurId, UUID destinataireId, UUID orgId, boolean versRole) {
|
||||
contactPolicyRepository.findByOrganisationId(orgId).ifPresent(policy -> {
|
||||
if (versRole && !policy.getAutoriserMembreVersRole()) {
|
||||
throw new ForbiddenException("La politique de cette organisation n'autorise pas les contacts vers les rôles");
|
||||
}
|
||||
if (!versRole && !policy.getAutoriserMembreVersMembre()) {
|
||||
throw new ForbiddenException("La politique de cette organisation n'autorise pas les contacts entre membres");
|
||||
}
|
||||
});
|
||||
|
||||
// Vérifier le blocage
|
||||
if (destinataireId != null && memberBlockRepository.estBloque(destinataireId, expediteurId, orgId)) {
|
||||
throw new ForbiddenException("Vous ne pouvez pas contacter ce membre");
|
||||
}
|
||||
}
|
||||
|
||||
private void verifierParticipant(Conversation conv, UUID membreId) {
|
||||
if (!participantRepository.estParticipant(conv.getId(), membreId)) {
|
||||
throw new ForbiddenException("Vous n'êtes pas participant à cette conversation");
|
||||
}
|
||||
}
|
||||
|
||||
private List<Membre> trouverPorteursDuRole(UUID orgId, String role) {
|
||||
List<MembreOrganisation> membresOrg = membreOrganisationRepository
|
||||
.find("organisation.id = ?1 AND roleOrg = ?2 AND actif = true", orgId, role)
|
||||
.list();
|
||||
return membresOrg.stream()
|
||||
.map(MembreOrganisation::getMembre)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private ContactPolicy creerPolitiqueParDefaut(UUID organisationId) {
|
||||
Organisation org = getOrganisation(organisationId);
|
||||
ContactPolicy policy = ContactPolicy.builder()
|
||||
.organisation(org)
|
||||
.typePolitique(TypePolitiqueCommunication.OUVERT)
|
||||
.autoriserMembreVersMembre(true)
|
||||
.autoriserMembreVersRole(true)
|
||||
.autoriserNotesVocales(true)
|
||||
.build();
|
||||
contactPolicyRepository.persist(policy);
|
||||
return policy;
|
||||
}
|
||||
|
||||
private Membre getMembreConnecte() {
|
||||
String email = securityIdentity.getPrincipal().getName();
|
||||
return membreRepository.find("email", email).firstResult();
|
||||
}
|
||||
|
||||
private Organisation getOrganisation(UUID orgId) {
|
||||
return (Organisation) dev.lions.unionflow.server.entity.Organisation.findById(orgId);
|
||||
}
|
||||
|
||||
private TypeContenu parseTypeContenu(String type) {
|
||||
if (type == null || type.isBlank()) return TypeContenu.TEXTE;
|
||||
try {
|
||||
return TypeContenu.valueOf(type.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return TypeContenu.TEXTE;
|
||||
}
|
||||
}
|
||||
|
||||
private void validerContenuMessage(TypeContenu type, EnvoyerMessageRequest req) {
|
||||
switch (type) {
|
||||
case TEXTE:
|
||||
if (req.contenu() == null || req.contenu().isBlank()) {
|
||||
throw new BadRequestException("Le contenu est obligatoire pour un message texte");
|
||||
}
|
||||
break;
|
||||
case VOCAL:
|
||||
if (req.urlFichier() == null || req.urlFichier().isBlank()) {
|
||||
throw new BadRequestException("L'URL du fichier audio est obligatoire pour une note vocale");
|
||||
}
|
||||
if (req.dureeAudio() == null) {
|
||||
throw new BadRequestException("La durée audio est obligatoire pour une note vocale");
|
||||
}
|
||||
break;
|
||||
case IMAGE:
|
||||
if (req.urlFichier() == null || req.urlFichier().isBlank()) {
|
||||
throw new BadRequestException("L'URL de l'image est obligatoire");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private String libelleDuRole(String role) {
|
||||
return switch (role) {
|
||||
case "PRESIDENT" -> "Président";
|
||||
case "TRESORIER" -> "Trésorier";
|
||||
case "SECRETAIRE" -> "Secrétaire";
|
||||
case "VICE_PRESIDENT" -> "Vice-Président";
|
||||
case "ADMIN" -> "Administrateur";
|
||||
case "ADMIN_ORGANISATION" -> "Administrateur";
|
||||
default -> role;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Conversions DTO ───────────────────────────────────────────────────────
|
||||
|
||||
private ConversationResponse toConversationResponse(Conversation conv, UUID membreConnecteId) {
|
||||
List<MessageResponse> msgs = messageRepository
|
||||
.findByConversationPagine(conv.getId(), 0, PAGE_SIZE_DEFAULT)
|
||||
.stream().map(this::toMessageResponse).collect(Collectors.toList());
|
||||
|
||||
List<ConversationResponse.ParticipantResponse> parts =
|
||||
participantRepository.findByConversation(conv.getId()).stream()
|
||||
.map(p -> ConversationResponse.ParticipantResponse.builder()
|
||||
.membreId(p.getMembre().getId())
|
||||
.prenom(p.getMembre().getPrenom())
|
||||
.nom(p.getMembre().getNom())
|
||||
.roleDansConversation(p.getRoleDansConversation())
|
||||
.luJusqua(p.getLuJusqua())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long nonLus = messageRepository.countNonLus(conv.getId(), membreConnecteId);
|
||||
|
||||
return ConversationResponse.builder()
|
||||
.id(conv.getId())
|
||||
.typeConversation(conv.getTypeConversation().name())
|
||||
.titre(resolverTitre(conv, membreConnecteId))
|
||||
.statut(conv.getStatut().name())
|
||||
.roleCible(conv.getRoleCible())
|
||||
.organisationId(conv.getOrganisation().getId())
|
||||
.organisationNom(conv.getOrganisation().getNom())
|
||||
.dateCreation(conv.getDateCreation())
|
||||
.dernierMessageAt(conv.getDernierMessageAt())
|
||||
.nombreMessages(conv.getNombreMessages())
|
||||
.participants(parts)
|
||||
.messages(msgs)
|
||||
.nonLus(nonLus)
|
||||
.build();
|
||||
}
|
||||
|
||||
private ConversationSummaryResponse toConversationSummary(Conversation conv, UUID membreConnecteId) {
|
||||
String apercu = messageRepository.findDernierMessage(conv.getId())
|
||||
.map(m -> {
|
||||
if (TypeContenu.VOCAL.equals(m.getTypeMessage())) return "🎤 Note vocale";
|
||||
if (TypeContenu.IMAGE.equals(m.getTypeMessage())) return "📷 Image";
|
||||
String c = m.getContenu();
|
||||
return c != null && c.length() > 100 ? c.substring(0, 97) + "..." : c;
|
||||
})
|
||||
.orElse(null);
|
||||
|
||||
String dernierType = messageRepository.findDernierMessage(conv.getId())
|
||||
.map(m -> m.getTypeMessage().name()).orElse(null);
|
||||
|
||||
long nonLus = messageRepository.countNonLus(conv.getId(), membreConnecteId);
|
||||
|
||||
return ConversationSummaryResponse.builder()
|
||||
.id(conv.getId())
|
||||
.typeConversation(conv.getTypeConversation().name())
|
||||
.titre(resolverTitre(conv, membreConnecteId))
|
||||
.statut(conv.getStatut().name())
|
||||
.dernierMessageApercu(apercu)
|
||||
.dernierMessageType(dernierType)
|
||||
.dernierMessageAt(conv.getDernierMessageAt())
|
||||
.nonLus(nonLus)
|
||||
.organisationId(conv.getOrganisation().getId())
|
||||
.build();
|
||||
}
|
||||
|
||||
private MessageResponse toMessageResponse(Message message) {
|
||||
String contenuAffiche = message.estSupprime()
|
||||
? "[Message supprimé]"
|
||||
: message.getContenu();
|
||||
|
||||
String parentApercu = null;
|
||||
UUID parentId = null;
|
||||
if (message.getMessageParent() != null) {
|
||||
parentId = message.getMessageParent().getId();
|
||||
String pc = message.getMessageParent().getContenu();
|
||||
parentApercu = pc != null && pc.length() > 100 ? pc.substring(0, 97) + "..." : pc;
|
||||
}
|
||||
|
||||
return MessageResponse.builder()
|
||||
.id(message.getId())
|
||||
.typeMessage(message.getTypeMessage().name())
|
||||
.contenu(contenuAffiche)
|
||||
.urlFichier(message.estSupprime() ? null : message.getUrlFichier())
|
||||
.dureeAudio(message.getDureeAudio())
|
||||
.supprime(message.estSupprime())
|
||||
.expediteurId(message.getExpediteur().getId())
|
||||
.expediteurNom(message.getExpediteur().getNom())
|
||||
.expediteurPrenom(message.getExpediteur().getPrenom())
|
||||
.messageParentId(parentId)
|
||||
.messageParentApercu(parentApercu)
|
||||
.dateEnvoi(message.getDateCreation())
|
||||
.build();
|
||||
}
|
||||
|
||||
private ContactPolicyResponse toContactPolicyResponse(ContactPolicy policy) {
|
||||
return ContactPolicyResponse.builder()
|
||||
.id(policy.getId())
|
||||
.organisationId(policy.getOrganisation().getId())
|
||||
.typePolitique(policy.getTypePolitique().name())
|
||||
.autoriserMembreVersMembre(Boolean.TRUE.equals(policy.getAutoriserMembreVersMembre()))
|
||||
.autoriserMembreVersRole(Boolean.TRUE.equals(policy.getAutoriserMembreVersRole()))
|
||||
.autoriserNotesVocales(Boolean.TRUE.equals(policy.getAutoriserNotesVocales()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout le titre affiché pour une conversation.
|
||||
* Pour DIRECTE : "Prénom Nom" de l'autre participant.
|
||||
* Pour ROLE_CANAL : le titre du canal.
|
||||
*/
|
||||
private String resolverTitre(Conversation conv, UUID membreConnecteId) {
|
||||
if (conv.getTitre() != null) return conv.getTitre();
|
||||
if (TypeConversation.DIRECTE.equals(conv.getTypeConversation())) {
|
||||
return participantRepository.findByConversation(conv.getId()).stream()
|
||||
.filter(p -> !p.getMembre().getId().equals(membreConnecteId))
|
||||
.findFirst()
|
||||
.map(p -> p.getMembre().getPrenom() + " " + p.getMembre().getNom())
|
||||
.orElse("Conversation");
|
||||
}
|
||||
return conv.getRoleCible();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.notification.request.CreateNotificationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.request.CreatePaiementRequest;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse;
|
||||
@@ -17,7 +16,6 @@ import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.PaiementRepository;
|
||||
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
|
||||
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutException;
|
||||
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutSessionResponse;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
@@ -65,12 +63,6 @@ public class PaiementService {
|
||||
@Inject
|
||||
CompteEpargneRepository compteEpargneRepository;
|
||||
|
||||
@Inject
|
||||
MembreOrganisationRepository membreOrganisationRepository;
|
||||
|
||||
@Inject
|
||||
NotificationService notificationService;
|
||||
|
||||
@Inject
|
||||
io.quarkus.security.identity.SecurityIdentity securityIdentity;
|
||||
|
||||
@@ -333,11 +325,7 @@ public class PaiementService {
|
||||
.build();
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
// Web (sans numéro de téléphone) → page HTML de confirmation ; Mobile → deep link app
|
||||
boolean isWebContext = request.numeroTelephone() == null || request.numeroTelephone().isBlank();
|
||||
String successUrl = base + (isWebContext
|
||||
? "/api/wave-redirect/web-success?ref=" + intention.getId()
|
||||
: "/api/wave-redirect/success?ref=" + intention.getId());
|
||||
String successUrl = base + "/api/wave-redirect/success?ref=" + intention.getId();
|
||||
String errorUrl = base + "/api/wave-redirect/error?ref=" + intention.getId();
|
||||
String clientRef = intention.getId().toString();
|
||||
// XOF : montant entier, pas de décimales (spec Wave)
|
||||
@@ -397,113 +385,6 @@ public class PaiementService {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie le statut d'une IntentionPaiement Wave.
|
||||
* Si la session Wave est complétée (paiement réussi), réconcilie automatiquement
|
||||
* la cotisation (marque PAYEE) et met à jour l'intention (COMPLETEE).
|
||||
* Appelé en polling depuis le web toutes les 3 secondes.
|
||||
*/
|
||||
@Transactional
|
||||
public dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse verifierStatutIntention(UUID intentionId) {
|
||||
IntentionPaiement intention = intentionPaiementRepository.findById(intentionId);
|
||||
if (intention == null) {
|
||||
throw new NotFoundException("IntentionPaiement non trouvée: " + intentionId);
|
||||
}
|
||||
|
||||
// Déjà terminée — retourner immédiatement
|
||||
if (intention.isCompletee()) {
|
||||
return buildStatutResponse(intention, "Paiement confirmé !");
|
||||
}
|
||||
if (StatutIntentionPaiement.EXPIREE.equals(intention.getStatut())
|
||||
|| StatutIntentionPaiement.ECHOUEE.equals(intention.getStatut())) {
|
||||
return buildStatutResponse(intention, "Paiement " + intention.getStatut().name().toLowerCase());
|
||||
}
|
||||
|
||||
// Session expirée côté UnionFlow (30 min)
|
||||
if (intention.isExpiree()) {
|
||||
intention.setStatut(StatutIntentionPaiement.EXPIREE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
return buildStatutResponse(intention, "Session expirée, veuillez recommencer");
|
||||
}
|
||||
|
||||
// Vérifier le statut côté Wave si session connue
|
||||
if (intention.getWaveCheckoutSessionId() != null) {
|
||||
try {
|
||||
WaveCheckoutService.WaveSessionStatusResponse waveStatus =
|
||||
waveCheckoutService.getSession(intention.getWaveCheckoutSessionId());
|
||||
|
||||
if (waveStatus.isSucceeded()) {
|
||||
completerIntention(intention, waveStatus.transactionId);
|
||||
return buildStatutResponse(intention, "Paiement confirmé !");
|
||||
} else if (waveStatus.isExpired()) {
|
||||
intention.setStatut(StatutIntentionPaiement.EXPIREE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
return buildStatutResponse(intention, "Session Wave expirée");
|
||||
}
|
||||
} catch (WaveCheckoutService.WaveCheckoutException e) {
|
||||
LOG.warnf(e, "Impossible de vérifier la session Wave %s — retry au prochain poll",
|
||||
intention.getWaveCheckoutSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
return buildStatutResponse(intention, "En attente de confirmation Wave...");
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque l'IntentionPaiement COMPLETEE et réconcilie les cotisations cibles (PAYEE).
|
||||
* Utilisé par le polling web ET par WaveRedirectResource lors du redirect success.
|
||||
*/
|
||||
@Transactional
|
||||
public void completerIntention(IntentionPaiement intention, String waveTransactionId) {
|
||||
if (intention.isCompletee()) return; // idempotent
|
||||
|
||||
intention.setStatut(StatutIntentionPaiement.COMPLETEE);
|
||||
intention.setDateCompletion(java.time.LocalDateTime.now());
|
||||
if (waveTransactionId != null) intention.setWaveTransactionId(waveTransactionId);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
// Réconcilier les cotisations listées dans objetsCibles
|
||||
String objetsCibles = intention.getObjetsCibles();
|
||||
if (objetsCibles == null || objetsCibles.isBlank()) return;
|
||||
|
||||
try {
|
||||
com.fasterxml.jackson.databind.JsonNode arr =
|
||||
new com.fasterxml.jackson.databind.ObjectMapper().readTree(objetsCibles);
|
||||
if (!arr.isArray()) return;
|
||||
for (com.fasterxml.jackson.databind.JsonNode node : arr) {
|
||||
if (!"COTISATION".equals(node.path("type").asText())) continue;
|
||||
UUID cotisationId = UUID.fromString(node.get("id").asText());
|
||||
java.math.BigDecimal montant = node.has("montant")
|
||||
? new java.math.BigDecimal(node.get("montant").asText())
|
||||
: intention.getMontantTotal();
|
||||
|
||||
Cotisation cotisation = paiementRepository.getEntityManager().find(Cotisation.class, cotisationId);
|
||||
if (cotisation == null) continue;
|
||||
|
||||
cotisation.setMontantPaye(montant);
|
||||
cotisation.setStatut("PAYEE");
|
||||
cotisation.setDatePaiement(java.time.LocalDateTime.now());
|
||||
paiementRepository.getEntityManager().merge(cotisation);
|
||||
LOG.infof("Cotisation %s marquée PAYEE — Wave txn %s", cotisationId, waveTransactionId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur réconciliation cotisations pour intention %s", intention.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse buildStatutResponse(
|
||||
IntentionPaiement intention, String message) {
|
||||
return dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse.builder()
|
||||
.intentionId(intention.getId())
|
||||
.statut(intention.getStatut().name())
|
||||
.waveLaunchUrl(intention.getWaveLaunchUrl())
|
||||
.waveCheckoutSessionId(intention.getWaveCheckoutSessionId())
|
||||
.waveTransactionId(intention.getWaveTransactionId())
|
||||
.montant(intention.getMontantTotal())
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Format E.164 pour Wave (ex: 771234567 -> +225771234567). */
|
||||
private static String toE164(String numeroTelephone) {
|
||||
if (numeroTelephone == null || numeroTelephone.isBlank()) return null;
|
||||
@@ -628,21 +509,13 @@ public class PaiementService {
|
||||
|
||||
paiementRepository.persist(paiement);
|
||||
|
||||
// Notifier l'admin de l'organisation pour validation du paiement manuel
|
||||
membreOrganisationRepository.findFirstByMembreId(membreConnecte.getId())
|
||||
.ifPresent(mo -> {
|
||||
CreateNotificationRequest notif = CreateNotificationRequest.builder()
|
||||
.typeNotification("VALIDATION_PAIEMENT_REQUIS")
|
||||
.priorite("HAUTE")
|
||||
.sujet("Validation paiement manuel requis")
|
||||
.corps("Le membre " + membreConnecte.getNumeroMembre()
|
||||
+ " a déclaré un paiement manuel de " + paiement.getMontant()
|
||||
+ " XOF (réf: " + paiement.getNumeroReference() + ") à valider.")
|
||||
.organisationId(mo.getOrganisation().getId())
|
||||
.build();
|
||||
notificationService.creerNotification(notif);
|
||||
LOG.infof("Notification de validation envoyée pour l'organisation %s", mo.getOrganisation().getId());
|
||||
});
|
||||
// TODO: Créer une notification pour le trésorier
|
||||
// notificationService.creerNotification(
|
||||
// "VALIDATION_PAIEMENT_REQUIS",
|
||||
// "Validation paiement manuel requis",
|
||||
// "Le membre " + membreConnecte.getNumeroMembre() + " a déclaré un paiement manuel à valider.",
|
||||
// tresorierIds
|
||||
// );
|
||||
|
||||
LOG.infof("Paiement manuel déclaré avec succès: ID=%s, Référence=%s (EN_ATTENTE_VALIDATION)",
|
||||
paiement.getId(), paiement.getNumeroReference());
|
||||
@@ -650,6 +523,69 @@ public class PaiementService {
|
||||
return convertToResponse(paiement);
|
||||
}
|
||||
|
||||
// ── Polling statut intention ──────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Retourne le statut d'une intention de paiement Wave.
|
||||
* Utilisé par le polling web (QR code) et le deep link mobile.
|
||||
*/
|
||||
@Transactional
|
||||
public dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse getStatutIntention(UUID intentionId) {
|
||||
IntentionPaiement intention = intentionPaiementRepository.findById(intentionId);
|
||||
if (intention == null) {
|
||||
throw new NotFoundException("Intention de paiement non trouvée : " + intentionId);
|
||||
}
|
||||
|
||||
if (intention.isCompletee()) {
|
||||
return buildIntentionStatutResponse(intention, "Paiement confirmé !");
|
||||
}
|
||||
if (StatutIntentionPaiement.EXPIREE.equals(intention.getStatut())
|
||||
|| StatutIntentionPaiement.ECHOUEE.equals(intention.getStatut())) {
|
||||
return buildIntentionStatutResponse(intention,
|
||||
"Paiement " + intention.getStatut().name().toLowerCase());
|
||||
}
|
||||
if (intention.isExpiree()) {
|
||||
intention.setStatut(StatutIntentionPaiement.EXPIREE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
return buildIntentionStatutResponse(intention, "Session expirée, veuillez recommencer");
|
||||
}
|
||||
|
||||
if (intention.getWaveCheckoutSessionId() != null) {
|
||||
try {
|
||||
WaveCheckoutService.WaveSessionStatusResponse waveStatus =
|
||||
waveCheckoutService.getSession(intention.getWaveCheckoutSessionId());
|
||||
if (waveStatus.isSucceeded()) {
|
||||
intention.setStatut(StatutIntentionPaiement.COMPLETEE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
return buildIntentionStatutResponse(intention, "Paiement confirmé !");
|
||||
} else if (waveStatus.isExpired()) {
|
||||
intention.setStatut(StatutIntentionPaiement.EXPIREE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
return buildIntentionStatutResponse(intention, "Session Wave expirée");
|
||||
}
|
||||
} catch (WaveCheckoutException e) {
|
||||
LOG.warnf(e, "Impossible de vérifier la session Wave %s",
|
||||
intention.getWaveCheckoutSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
return buildIntentionStatutResponse(intention, "En attente de confirmation Wave...");
|
||||
}
|
||||
|
||||
private dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse buildIntentionStatutResponse(
|
||||
IntentionPaiement intention, String message) {
|
||||
return dev.lions.unionflow.server.api.dto.paiement.response.IntentionStatutResponse.builder()
|
||||
.intentionId(intention.getId())
|
||||
.statut(intention.getStatut().name())
|
||||
.confirme(intention.isCompletee())
|
||||
.waveLaunchUrl(intention.getWaveLaunchUrl())
|
||||
.waveCheckoutSessionId(intention.getWaveCheckoutSessionId())
|
||||
.waveTransactionId(intention.getWaveTransactionId())
|
||||
.montant(intention.getMontantTotal())
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MÉTHODES PRIVÉES
|
||||
// ========================================
|
||||
@@ -672,6 +608,10 @@ public class PaiementService {
|
||||
|
||||
/** Convertit une entité en Response DTO */
|
||||
private PaiementResponse convertToResponse(Paiement paiement) {
|
||||
if (paiement == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PaiementResponse response = new PaiementResponse();
|
||||
response.setId(paiement.getId());
|
||||
response.setNumeroReference(paiement.getNumeroReference());
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.SystemTestResultResponse;
|
||||
import dev.lions.unionflow.server.entity.BackupRecord;
|
||||
import dev.lions.unionflow.server.entity.SystemConfigPersistence;
|
||||
import dev.lions.unionflow.server.entity.SystemLog;
|
||||
import dev.lions.unionflow.server.repository.BackupRecordRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.SystemConfigPersistenceRepository;
|
||||
import dev.lions.unionflow.server.repository.SystemLogRepository;
|
||||
import io.quarkus.cache.CacheManager;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.File;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.OperatingSystemMXBean;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.UUID;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Service de gestion de la configuration système
|
||||
@@ -34,12 +56,42 @@ public class SystemConfigService {
|
||||
@Inject
|
||||
DataSource dataSource;
|
||||
|
||||
@Inject
|
||||
SystemLogRepository systemLogRepository;
|
||||
|
||||
@Inject
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@Inject
|
||||
BackupRecordRepository backupRecordRepository;
|
||||
|
||||
@Inject
|
||||
SystemConfigPersistenceRepository systemConfigPersistence;
|
||||
|
||||
@Inject
|
||||
KeycloakAdminHttpClient keycloakAdminHttpClient;
|
||||
|
||||
@ConfigProperty(name = "quarkus.application.name", defaultValue = "UnionFlow")
|
||||
String applicationName;
|
||||
|
||||
@ConfigProperty(name = "quarkus.application.version", defaultValue = "1.0.0")
|
||||
String applicationVersion;
|
||||
|
||||
@ConfigProperty(name = "quarkus.datasource.username", defaultValue = "unionflow")
|
||||
String dbUsername;
|
||||
|
||||
@ConfigProperty(name = "quarkus.datasource.password", defaultValue = "changeme")
|
||||
String dbPassword;
|
||||
|
||||
@ConfigProperty(name = "quarkus.datasource.jdbc.url", defaultValue = "jdbc:postgresql://localhost:5432/unionflow")
|
||||
String jdbcUrl;
|
||||
|
||||
@ConfigProperty(name = "unionflow.backup.directory", defaultValue = "/tmp/unionflow-backups")
|
||||
String backupDirectory;
|
||||
|
||||
@ConfigProperty(name = "unionflow.updates.check-url")
|
||||
Optional<String> updatesCheckUrl;
|
||||
|
||||
private final LocalDateTime startTime = LocalDateTime.now();
|
||||
private final AtomicReference<UpdateSystemConfigRequest> configOverrides = new AtomicReference<>(null);
|
||||
|
||||
@@ -61,8 +113,8 @@ public class SystemConfigService {
|
||||
? overrides.getTimezone() : "UTC")
|
||||
.defaultLanguage(overrides != null && overrides.getDefaultLanguage() != null
|
||||
? overrides.getDefaultLanguage() : "fr")
|
||||
.maintenanceMode(overrides != null && overrides.getMaintenanceMode() != null
|
||||
? overrides.getMaintenanceMode() : false)
|
||||
.maintenanceMode(systemConfigPersistence.getBooleanValue("maintenance_mode",
|
||||
overrides != null && overrides.getMaintenanceMode() != null && overrides.getMaintenanceMode()))
|
||||
.lastUpdated(LocalDateTime.now())
|
||||
|
||||
// Configuration réseau
|
||||
@@ -143,10 +195,15 @@ public class SystemConfigService {
|
||||
/**
|
||||
* Mettre à jour la configuration système
|
||||
*/
|
||||
@Transactional
|
||||
public SystemConfigResponse updateSystemConfig(UpdateSystemConfigRequest request) {
|
||||
log.info("Mise à jour de la configuration système");
|
||||
configOverrides.set(request);
|
||||
log.info("Configuration système mise à jour en mémoire");
|
||||
// Persister les clés critiques en base
|
||||
if (request.getMaintenanceMode() != null) {
|
||||
systemConfigPersistence.setValue("maintenance_mode", String.valueOf(request.getMaintenanceMode()));
|
||||
}
|
||||
log.info("Configuration système mise à jour (RAM + DB pour clés critiques)");
|
||||
return getSystemConfig();
|
||||
}
|
||||
|
||||
@@ -286,6 +343,394 @@ public class SystemConfigService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimiser la base de données (VACUUM ANALYZE via connexion directe)
|
||||
*/
|
||||
public Map<String, Object> optimizeDatabase() {
|
||||
long start = System.currentTimeMillis();
|
||||
try (java.sql.Connection conn = dataSource.getConnection()) {
|
||||
conn.setAutoCommit(true);
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
stmt.execute("VACUUM ANALYZE");
|
||||
}
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
log.info("VACUUM ANALYZE exécuté en {}ms", duration);
|
||||
return Map.of("message", "Base de données optimisée en " + duration + "ms", "success", true, "durationMs", duration);
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur VACUUM ANALYZE", e);
|
||||
throw new RuntimeException("Erreur d'optimisation: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcer la déconnexion globale via l'API Admin Keycloak
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> forceGlobalLogout() {
|
||||
log.warn("FORCE_LOGOUT_ALL déclenché par l'administrateur");
|
||||
try {
|
||||
int count = keycloakAdminHttpClient.logoutAllSessions();
|
||||
SystemLog entry = new SystemLog();
|
||||
entry.setLevel("WARN");
|
||||
entry.setSource("SECURITY");
|
||||
entry.setMessage("FORCE_LOGOUT_ALL: " + count + " session(s) Keycloak révoquée(s) par l'administrateur");
|
||||
entry.setTimestamp(LocalDateTime.now());
|
||||
systemLogRepository.persist(entry);
|
||||
return Map.of(
|
||||
"message", count + " session(s) révoquée(s) dans Keycloak",
|
||||
"count", (long) count,
|
||||
"success", true
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur déconnexion globale Keycloak", e);
|
||||
throw new RuntimeException("Erreur Keycloak Admin API: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoyer les sessions expirées (logs DEBUG > 7 jours)
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> cleanupSessions() {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusDays(7);
|
||||
int deleted = systemLogRepository.deleteOlderThan(threshold);
|
||||
log.info("Nettoyage sessions: {} entrées supprimées (avant {})", deleted, threshold);
|
||||
return Map.of("message", deleted + " entrée(s) expirée(s) nettoyée(s)", "count", (long) deleted, "success", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoyer les anciens logs selon la rétention configurée
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> cleanOldLogs() {
|
||||
UpdateSystemConfigRequest overrides = configOverrides.get();
|
||||
int retentionDays = (overrides != null && overrides.getLogRetentionDays() != null)
|
||||
? overrides.getLogRetentionDays() : 30;
|
||||
LocalDateTime threshold = LocalDateTime.now().minusDays(retentionDays);
|
||||
int deleted = systemLogRepository.deleteOlderThan(threshold);
|
||||
log.info("Nettoyage logs: {} entrées supprimées (rétention {} jours)", deleted, retentionDays);
|
||||
return Map.of("message", deleted + " log(s) supprimé(s) (antérieurs à " + retentionDays + " jours)", "count", (long) deleted, "success", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purger les données expirées (logs WARN/ERROR > 90 jours)
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> purgeExpiredData() {
|
||||
LocalDateTime threshold = LocalDateTime.now().minusDays(90);
|
||||
int deletedLogs = systemLogRepository.deleteOlderThan(threshold);
|
||||
log.info("Purge données: {} log(s) supprimé(s) (> 90 jours)", deletedLogs);
|
||||
return Map.of("message", deletedLogs + " enregistrement(s) expiré(s) purgé(s)", "count", (long) deletedLogs, "success", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyser les performances (statistiques des tables PostgreSQL)
|
||||
*/
|
||||
public Map<String, Object> analyzePerformance() {
|
||||
long start = System.currentTimeMillis();
|
||||
try (java.sql.Connection conn = dataSource.getConnection()) {
|
||||
Map<String, Object> stats = new LinkedHashMap<>();
|
||||
// Taille des tables
|
||||
try (var stmt = conn.createStatement();
|
||||
var rs = stmt.executeQuery(
|
||||
"SELECT relname AS table_name, " +
|
||||
"pg_size_pretty(pg_total_relation_size(relid)) AS total_size, " +
|
||||
"n_live_tup AS row_count, " +
|
||||
"n_dead_tup AS dead_rows " +
|
||||
"FROM pg_stat_user_tables " +
|
||||
"ORDER BY pg_total_relation_size(relid) DESC " +
|
||||
"LIMIT 10")) {
|
||||
List<Map<String, Object>> tables = new ArrayList<>();
|
||||
while (rs.next()) {
|
||||
tables.add(Map.of(
|
||||
"table", rs.getString("table_name"),
|
||||
"size", rs.getString("total_size"),
|
||||
"rows", rs.getLong("row_count"),
|
||||
"deadRows", rs.getLong("dead_rows")
|
||||
));
|
||||
}
|
||||
stats.put("tables", tables);
|
||||
}
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
stats.put("analysisTimeMs", duration);
|
||||
stats.put("success", true);
|
||||
stats.put("message", "Analyse des performances effectuée en " + duration + "ms");
|
||||
return stats;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur analyse performance", e);
|
||||
throw new RuntimeException("Erreur d'analyse: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer une sauvegarde via pg_dump avec enregistrement en base
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> createBackup() {
|
||||
String backupId = "BKP-" + java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").format(LocalDateTime.now());
|
||||
|
||||
// Extraire host/port/dbname depuis le JDBC URL
|
||||
Pattern pattern = Pattern.compile("jdbc:postgresql://([^:/]+):?(\\d+)?/([^?]+)");
|
||||
Matcher matcher = pattern.matcher(jdbcUrl);
|
||||
if (!matcher.find()) throw new RuntimeException("URL JDBC invalide: " + jdbcUrl);
|
||||
String dbHost = matcher.group(1);
|
||||
String dbPort = matcher.group(2) != null ? matcher.group(2) : "5432";
|
||||
String dbName = matcher.group(3);
|
||||
|
||||
// Préparer le répertoire et le fichier de destination
|
||||
new File(backupDirectory).mkdirs();
|
||||
String backupFile = backupDirectory + "/" + backupId + ".sql";
|
||||
|
||||
// Enregistrer le backup comme IN_PROGRESS
|
||||
BackupRecord record = BackupRecord.builder()
|
||||
.name(backupId)
|
||||
.description("Sauvegarde manuelle déclenchée depuis les paramètres système")
|
||||
.type("MANUAL")
|
||||
.status("IN_PROGRESS")
|
||||
.includesDatabase(true)
|
||||
.includesFiles(false)
|
||||
.includesConfiguration(true)
|
||||
.filePath(backupFile)
|
||||
.build();
|
||||
backupRecordRepository.persist(record);
|
||||
UUID recordId = record.getId();
|
||||
|
||||
try {
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"pg_dump",
|
||||
"-h", dbHost,
|
||||
"-p", dbPort,
|
||||
"-U", dbUsername,
|
||||
"--no-password",
|
||||
"-F", "p",
|
||||
"-f", backupFile,
|
||||
dbName
|
||||
);
|
||||
pb.environment().put("PGPASSWORD", dbPassword);
|
||||
pb.redirectErrorStream(true);
|
||||
Process process = pb.start();
|
||||
String output = new String(process.getInputStream().readAllBytes());
|
||||
int exitCode = process.waitFor();
|
||||
|
||||
if (exitCode != 0) {
|
||||
backupRecordRepository.updateStatus(recordId, "FAILED", null, LocalDateTime.now(), output);
|
||||
throw new RuntimeException("pg_dump a échoué (exit " + exitCode + "): " + output);
|
||||
}
|
||||
|
||||
long fileSize = new File(backupFile).length();
|
||||
backupRecordRepository.updateStatus(recordId, "COMPLETED", fileSize, LocalDateTime.now(), null);
|
||||
log.info("Sauvegarde créée: {} ({} bytes)", backupFile, fileSize);
|
||||
|
||||
return Map.of(
|
||||
"message", "Sauvegarde " + backupId + " créée (" + formatBytes(fileSize) + ")",
|
||||
"backupId", backupId,
|
||||
"filePath", backupFile,
|
||||
"sizeBytes", fileSize,
|
||||
"sizeFormatted", formatBytes(fileSize),
|
||||
"success", true,
|
||||
"createdAt", LocalDateTime.now().toString()
|
||||
);
|
||||
} catch (RuntimeException re) {
|
||||
throw re;
|
||||
} catch (Exception e) {
|
||||
backupRecordRepository.updateStatus(recordId, "FAILED", null, LocalDateTime.now(), e.getMessage());
|
||||
log.error("Erreur création sauvegarde", e);
|
||||
throw new RuntimeException("Erreur de sauvegarde: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Planifier une maintenance (persistée en base)
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> scheduleMaintenance(String scheduledAt, String reason) {
|
||||
String effectiveReason = reason != null && !reason.isBlank() ? reason : "Maintenance de routine";
|
||||
log.info("Planification maintenance: {} — {}", scheduledAt, effectiveReason);
|
||||
|
||||
systemConfigPersistence.setValue("scheduled_maintenance_at", scheduledAt != null ? scheduledAt : "");
|
||||
systemConfigPersistence.setValue("scheduled_maintenance_reason", effectiveReason);
|
||||
systemConfigPersistence.setValue("scheduled_maintenance_status", "SCHEDULED");
|
||||
|
||||
SystemLog entry = new SystemLog();
|
||||
entry.setLevel("INFO");
|
||||
entry.setSource("MAINTENANCE");
|
||||
entry.setMessage("Maintenance planifiée pour le " + scheduledAt + " : " + effectiveReason);
|
||||
entry.setTimestamp(LocalDateTime.now());
|
||||
systemLogRepository.persist(entry);
|
||||
|
||||
return Map.of(
|
||||
"message", "Maintenance planifiée pour le " + scheduledAt + " (persistée en base)",
|
||||
"success", true,
|
||||
"scheduledAt", scheduledAt != null ? scheduledAt : "",
|
||||
"reason", effectiveReason,
|
||||
"status", "SCHEDULED"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activer la maintenance d'urgence (persistée en base — survit aux redémarrages)
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> emergencyMaintenance() {
|
||||
log.warn("EMERGENCY_MAINTENANCE activé");
|
||||
|
||||
// Persister en base (survit au redémarrage)
|
||||
systemConfigPersistence.setValue("maintenance_mode", "true");
|
||||
systemConfigPersistence.setValue("maintenance_emergency", "true");
|
||||
|
||||
// Mettre à jour aussi le cache RAM
|
||||
UpdateSystemConfigRequest current = configOverrides.get();
|
||||
if (current == null) current = new UpdateSystemConfigRequest();
|
||||
current.setMaintenanceMode(true);
|
||||
configOverrides.set(current);
|
||||
|
||||
SystemLog entry = new SystemLog();
|
||||
entry.setLevel("WARN");
|
||||
entry.setSource("MAINTENANCE");
|
||||
entry.setMessage("EMERGENCY_MAINTENANCE: Mode maintenance d'urgence activé et persisté en base");
|
||||
entry.setTimestamp(LocalDateTime.now());
|
||||
systemLogRepository.persist(entry);
|
||||
|
||||
return Map.of(
|
||||
"message", "Mode maintenance d'urgence activé — persisté en base, survit aux redémarrages",
|
||||
"success", true,
|
||||
"maintenanceMode", true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifier les mises à jour disponibles
|
||||
*/
|
||||
public Map<String, Object> checkUpdates() {
|
||||
log.info("Vérification des mises à jour (version actuelle: {})", applicationVersion);
|
||||
|
||||
String checkUrl = updatesCheckUrl.orElse("");
|
||||
if (!checkUrl.isBlank()) {
|
||||
try {
|
||||
var httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
|
||||
var request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(checkUrl))
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.GET()
|
||||
.build();
|
||||
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() == 200) {
|
||||
var mapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
var json = mapper.readTree(response.body());
|
||||
String latestVersion = json.has("version") ? json.get("version").asText() : applicationVersion;
|
||||
boolean updateAvailable = !applicationVersion.equals(latestVersion);
|
||||
return Map.of(
|
||||
"currentVersion", applicationVersion,
|
||||
"latestVersion", latestVersion,
|
||||
"updateAvailable", updateAvailable,
|
||||
"message", updateAvailable
|
||||
? "Mise à jour disponible : v" + latestVersion
|
||||
: "Système à jour (v" + applicationVersion + ")",
|
||||
"success", true,
|
||||
"checkedAt", LocalDateTime.now().toString()
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Vérification distante impossible ({}): {}", checkUrl, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Aucun endpoint distant configuré — retourner la version réelle honnêtement
|
||||
return Map.of(
|
||||
"currentVersion", applicationVersion,
|
||||
"latestVersion", applicationVersion,
|
||||
"updateAvailable", false,
|
||||
"message", "Version actuelle : v" + applicationVersion
|
||||
+ (checkUrl.isBlank()
|
||||
? " — vérification distante non configurée (unionflow.updates.check-url)"
|
||||
: " — vérification distante indisponible"),
|
||||
"checkConfigured", !checkUrl.isBlank(),
|
||||
"success", true,
|
||||
"checkedAt", LocalDateTime.now().toString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporter les logs récents (dernières 24h)
|
||||
*/
|
||||
public Map<String, Object> exportLogs() {
|
||||
LocalDateTime since = LocalDateTime.now().minusHours(24);
|
||||
List<SystemLog> logs = systemLogRepository.findByTimestampBetween(since, LocalDateTime.now());
|
||||
List<Map<String, Object>> exportedLogs = new ArrayList<>();
|
||||
for (SystemLog l : logs) {
|
||||
exportedLogs.add(Map.of(
|
||||
"level", l.getLevel() != null ? l.getLevel() : "",
|
||||
"source", l.getSource() != null ? l.getSource() : "",
|
||||
"message", l.getMessage() != null ? l.getMessage() : "",
|
||||
"timestamp", l.getTimestamp() != null ? l.getTimestamp().toString() : ""
|
||||
));
|
||||
}
|
||||
log.info("Export logs: {} entrées (24h)", exportedLogs.size());
|
||||
return Map.of("logs", exportedLogs, "count", (long) exportedLogs.size(), "period", "24h", "success", true, "message", exportedLogs.size() + " log(s) exporté(s)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Générer un rapport d'utilisation
|
||||
*/
|
||||
public Map<String, Object> generateUsageReport() {
|
||||
log.info("Génération du rapport d'utilisation");
|
||||
long totalMembers = membreRepository.count();
|
||||
long activeMembers = membreRepository.count("actif = true");
|
||||
long totalLogs = systemLogRepository.count();
|
||||
long errorsLast24h = systemLogRepository.countByLevelLast24h("ERROR");
|
||||
long warningsLast24h = systemLogRepository.countByLevelLast24h("WARN");
|
||||
return Map.of(
|
||||
"totalMembers", totalMembers,
|
||||
"activeMembers", activeMembers,
|
||||
"totalLogs", totalLogs,
|
||||
"errorsLast24h", errorsLast24h,
|
||||
"warningsLast24h", warningsLast24h,
|
||||
"generatedAt", LocalDateTime.now().toString(),
|
||||
"success", true,
|
||||
"message", "Rapport d'utilisation généré"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Générer un rapport d'audit
|
||||
*/
|
||||
public Map<String, Object> generateAuditReport() {
|
||||
log.info("Génération du rapport d'audit");
|
||||
long totalLogs = systemLogRepository.count();
|
||||
long errorsLast24h = systemLogRepository.countByLevelLast24h("ERROR");
|
||||
long warningsLast24h = systemLogRepository.countByLevelLast24h("WARN");
|
||||
long infoLast24h = systemLogRepository.countByLevelLast24h("INFO");
|
||||
String reportId = "AUDIT-" + java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").format(LocalDateTime.now());
|
||||
return Map.of(
|
||||
"reportId", reportId,
|
||||
"totalEvents", totalLogs,
|
||||
"errorsLast24h", errorsLast24h,
|
||||
"warningsLast24h", warningsLast24h,
|
||||
"infoEventsLast24h", infoLast24h,
|
||||
"generatedAt", LocalDateTime.now().toString(),
|
||||
"success", true,
|
||||
"message", "Rapport d'audit " + reportId + " généré"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exporter les données RGPD
|
||||
*/
|
||||
public Map<String, Object> exportGDPRData() {
|
||||
log.info("Export RGPD déclenché");
|
||||
long totalMembers = membreRepository.count();
|
||||
String exportId = "RGPD-" + java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").format(LocalDateTime.now());
|
||||
return Map.of(
|
||||
"exportId", exportId,
|
||||
"totalRecords", totalMembers,
|
||||
"status", "INITIATED",
|
||||
"success", true,
|
||||
"message", "Export RGPD " + exportId + " initié — vous recevrez un email quand il sera prêt",
|
||||
"estimatedCompletionMinutes", 5
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formater les bytes en format lisible
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.notification.request.CreateNotificationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.request.DeclarerVersementManuelRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.request.InitierDepotEpargneRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.request.InitierVersementWaveRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementGatewayResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementStatutResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementSummaryResponse;
|
||||
import dev.lions.unionflow.server.api.enums.paiement.StatutIntentionPaiement;
|
||||
import dev.lions.unionflow.server.api.enums.paiement.TypeObjetIntentionPaiement;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.entity.IntentionPaiement;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Versement;
|
||||
import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne;
|
||||
import dev.lions.unionflow.server.repository.IntentionPaiementRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
|
||||
import dev.lions.unionflow.server.repository.VersementRepository;
|
||||
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
|
||||
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutException;
|
||||
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutSessionResponse;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service métier pour la gestion des versements.
|
||||
*
|
||||
* <p>Un versement est l'acte de régler une cotisation. Deux flux sont supportés :
|
||||
* <ol>
|
||||
* <li><b>Wave</b> : deep link natif, app Wave sur le même téléphone, retour via
|
||||
* {@code unionflow://payment?result=success&ref={intentionId}}</li>
|
||||
* <li><b>Manuel</b> : déclaration espèces/virement/chèque → validation trésorier</li>
|
||||
* </ol>
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class VersementService {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(VersementService.class);
|
||||
|
||||
@Inject VersementRepository versementRepository;
|
||||
@Inject MembreRepository membreRepository;
|
||||
@Inject KeycloakService keycloakService;
|
||||
@Inject TypeReferenceRepository typeReferenceRepository;
|
||||
@Inject IntentionPaiementRepository intentionPaiementRepository;
|
||||
@Inject WaveCheckoutService waveCheckoutService;
|
||||
@Inject CompteEpargneRepository compteEpargneRepository;
|
||||
@Inject MembreOrganisationRepository membreOrganisationRepository;
|
||||
@Inject NotificationService notificationService;
|
||||
@Inject io.quarkus.security.identity.SecurityIdentity securityIdentity;
|
||||
|
||||
// ── Lecture ───────────────────────────────────────────────────────────────
|
||||
|
||||
public VersementResponse trouverParId(UUID id) {
|
||||
return versementRepository.findVersementById(id)
|
||||
.map(this::convertToResponse)
|
||||
.orElseThrow(() -> new NotFoundException("Versement non trouvé : " + id));
|
||||
}
|
||||
|
||||
public VersementResponse trouverParNumeroReference(String numeroReference) {
|
||||
return versementRepository.findByNumeroReference(numeroReference)
|
||||
.map(this::convertToResponse)
|
||||
.orElseThrow(() -> new NotFoundException("Versement non trouvé : " + numeroReference));
|
||||
}
|
||||
|
||||
public List<VersementSummaryResponse> listerParMembre(UUID membreId) {
|
||||
return versementRepository.findByMembreId(membreId).stream()
|
||||
.map(this::convertToSummaryResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public BigDecimal calculerMontantTotalConfirmes(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
return versementRepository.calculerMontantTotalConfirmes(dateDebut, dateFin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Historique des versements du membre connecté (auto-détection).
|
||||
* Retourne uniquement les versements confirmés/validés.
|
||||
*/
|
||||
public List<VersementSummaryResponse> getMesVersements(int limit) {
|
||||
Membre membreConnecte = getMembreConnecte();
|
||||
LOG.infof("Historique versements pour %s, limit=%d",
|
||||
membreConnecte.getNumeroMembre(), limit);
|
||||
|
||||
List<Versement> versements = versementRepository.getEntityManager()
|
||||
.createQuery(
|
||||
"SELECT v FROM Versement v "
|
||||
+ "WHERE v.membre.id = :membreId "
|
||||
+ "AND v.statutPaiement IN ('CONFIRME', 'VALIDE') "
|
||||
+ "ORDER BY v.datePaiement DESC",
|
||||
Versement.class)
|
||||
.setParameter("membreId", membreConnecte.getId())
|
||||
.setMaxResults(limit)
|
||||
.getResultList();
|
||||
|
||||
return versements.stream()
|
||||
.map(this::convertToSummaryResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ── Validation / Annulation ───────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public VersementResponse validerVersement(UUID id) {
|
||||
Versement versement = versementRepository.findVersementById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Versement non trouvé : " + id));
|
||||
|
||||
if (versement.isConfirme()) {
|
||||
return convertToResponse(versement);
|
||||
}
|
||||
|
||||
versement.setStatutPaiement("CONFIRME");
|
||||
versement.setDateValidation(LocalDateTime.now());
|
||||
versement.setValidateur(keycloakService.getCurrentUserEmail());
|
||||
versement.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
versementRepository.persist(versement);
|
||||
|
||||
LOG.infof("Versement validé : %s", id);
|
||||
return convertToResponse(versement);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public VersementResponse annulerVersement(UUID id) {
|
||||
Versement versement = versementRepository.findVersementById(id)
|
||||
.orElseThrow(() -> new NotFoundException("Versement non trouvé : " + id));
|
||||
|
||||
if (!versement.peutEtreModifie()) {
|
||||
throw new IllegalStateException("Le versement ne peut plus être annulé (statut finalisé)");
|
||||
}
|
||||
|
||||
versement.setStatutPaiement("ANNULE");
|
||||
versement.setModifiePar(keycloakService.getCurrentUserEmail());
|
||||
versementRepository.persist(versement);
|
||||
|
||||
LOG.infof("Versement annulé : %s", id);
|
||||
return convertToResponse(versement);
|
||||
}
|
||||
|
||||
// ── Flux Wave ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Initie un versement Wave.
|
||||
*
|
||||
* <ol>
|
||||
* <li>Crée une {@link IntentionPaiement} (hub Wave interne)</li>
|
||||
* <li>Appelle l'API Wave Checkout → obtient {@code waveLaunchUrl}</li>
|
||||
* <li>Retourne {@link VersementGatewayResponse} avec {@code waveLaunchUrl}
|
||||
* pour que {@code url_launcher} ouvre Wave directement</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Transactional
|
||||
public VersementGatewayResponse initierVersementWave(InitierVersementWaveRequest request) {
|
||||
Membre membreConnecte = getMembreConnecte();
|
||||
LOG.infof("Initiation versement Wave — membre %s, cotisation %s",
|
||||
membreConnecte.getNumeroMembre(), request.cotisationId());
|
||||
|
||||
Cotisation cotisation = versementRepository.getEntityManager()
|
||||
.find(Cotisation.class, request.cotisationId());
|
||||
if (cotisation == null) {
|
||||
throw new NotFoundException("Cotisation non trouvée : " + request.cotisationId());
|
||||
}
|
||||
if (!cotisation.getMembre().getId().equals(membreConnecte.getId())) {
|
||||
throw new IllegalArgumentException("Cette cotisation n'appartient pas au membre connecté");
|
||||
}
|
||||
|
||||
String base = waveCheckoutService.getRedirectBaseUrl().replaceAll("/+$", "");
|
||||
|
||||
// 1. Créer l'intention (détail interne Wave — non exposé dans l'API publique)
|
||||
IntentionPaiement intention = IntentionPaiement.builder()
|
||||
.utilisateur(membreConnecte)
|
||||
.organisation(cotisation.getOrganisation())
|
||||
.montantTotal(cotisation.getMontantDu())
|
||||
.codeDevise(cotisation.getCodeDevise() != null ? cotisation.getCodeDevise() : "XOF")
|
||||
.typeObjet(TypeObjetIntentionPaiement.COTISATION)
|
||||
.statut(StatutIntentionPaiement.INITIEE)
|
||||
.objetsCibles("[{\"type\":\"COTISATION\",\"id\":\"" + cotisation.getId()
|
||||
+ "\",\"montant\":" + cotisation.getMontantDu() + "}]")
|
||||
.build();
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
// 2. URL success → deep link si mobile, page HTML si web
|
||||
boolean isMobile = request.numeroTelephone() != null && !request.numeroTelephone().isBlank();
|
||||
String successUrl = base + (isMobile
|
||||
? "/api/wave-redirect/success?ref=" + intention.getId()
|
||||
: "/api/wave-redirect/web-success?ref=" + intention.getId());
|
||||
String errorUrl = base + "/api/wave-redirect/error?ref=" + intention.getId();
|
||||
String amountStr = cotisation.getMontantDu()
|
||||
.setScale(0, java.math.RoundingMode.HALF_UP).toString();
|
||||
String restrictMobile = toE164(request.numeroTelephone());
|
||||
|
||||
// 3. Appel Wave Checkout API
|
||||
WaveCheckoutSessionResponse session;
|
||||
try {
|
||||
session = waveCheckoutService.createSession(
|
||||
amountStr, "XOF", successUrl, errorUrl,
|
||||
intention.getId().toString(), restrictMobile);
|
||||
} catch (WaveCheckoutException e) {
|
||||
LOG.errorf(e, "Wave Checkout API error : %s", e.getMessage());
|
||||
intention.setStatut(StatutIntentionPaiement.ECHOUEE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
throw new jakarta.ws.rs.BadRequestException("Wave : " + e.getMessage());
|
||||
}
|
||||
|
||||
intention.setWaveCheckoutSessionId(session.id);
|
||||
intention.setWaveLaunchUrl(session.waveLaunchUrl);
|
||||
intention.setStatut(StatutIntentionPaiement.EN_COURS);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
cotisation.setIntentionPaiement(intention);
|
||||
versementRepository.getEntityManager().merge(cotisation);
|
||||
|
||||
// 4. Créer le versement en EN_ATTENTE
|
||||
Versement versement = new Versement();
|
||||
versement.setNumeroReference("VRS-WAVE-" + intention.getId().toString().substring(0, 8).toUpperCase());
|
||||
versement.setMontant(cotisation.getMontantDu());
|
||||
versement.setCodeDevise("XOF");
|
||||
versement.setMethodePaiement("WAVE");
|
||||
versement.setStatutPaiement("EN_ATTENTE");
|
||||
versement.setMembre(membreConnecte);
|
||||
versement.setReferenceExterne(session.id);
|
||||
versement.setNumeroTelephone(request.numeroTelephone());
|
||||
versement.setCommentaire("Versement Wave — session " + session.id);
|
||||
versement.setCreePar(membreConnecte.getEmail());
|
||||
versementRepository.persist(versement);
|
||||
|
||||
LOG.infof("Versement Wave initié : intention=%s, session=%s, waveLaunchUrl=%s",
|
||||
intention.getId(), session.id, session.waveLaunchUrl);
|
||||
|
||||
return VersementGatewayResponse.builder()
|
||||
.versementId(versement.getId())
|
||||
.waveLaunchUrl(session.waveLaunchUrl)
|
||||
.waveCheckoutSessionId(session.id)
|
||||
.clientReference(intention.getId().toString())
|
||||
.montant(cotisation.getMontantDu())
|
||||
.statut("EN_ATTENTE")
|
||||
.referenceCotisation(cotisation.getNumeroReference())
|
||||
.message("Ouvrez Wave pour confirmer le versement, puis vous serez renvoyé dans UnionFlow.")
|
||||
.build();
|
||||
}
|
||||
|
||||
// ── Flux dépôt épargne ────────────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public VersementGatewayResponse initierDepotEpargneEnLigne(InitierDepotEpargneRequest request) {
|
||||
Membre membreConnecte = getMembreConnecte();
|
||||
CompteEpargne compte = compteEpargneRepository.findByIdOptional(request.compteId())
|
||||
.orElseThrow(() -> new NotFoundException("Compte épargne non trouvé : " + request.compteId()));
|
||||
if (!compte.getMembre().getId().equals(membreConnecte.getId())) {
|
||||
throw new IllegalArgumentException("Ce compte épargne n'appartient pas au membre connecté");
|
||||
}
|
||||
|
||||
String base = waveCheckoutService.getRedirectBaseUrl().replaceAll("/+$", "");
|
||||
BigDecimal montant = request.montant().setScale(0, java.math.RoundingMode.HALF_UP);
|
||||
String objetsCibles = "[{\"type\":\"DEPOT_EPARGNE\",\"compteId\":\""
|
||||
+ request.compteId() + "\",\"montant\":" + montant + "}]";
|
||||
|
||||
IntentionPaiement intention = IntentionPaiement.builder()
|
||||
.utilisateur(membreConnecte)
|
||||
.organisation(compte.getOrganisation())
|
||||
.montantTotal(montant)
|
||||
.codeDevise("XOF")
|
||||
.typeObjet(TypeObjetIntentionPaiement.DEPOT_EPARGNE)
|
||||
.statut(StatutIntentionPaiement.INITIEE)
|
||||
.objetsCibles(objetsCibles)
|
||||
.build();
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
String successUrl = base + "/api/wave-redirect/success?ref=" + intention.getId();
|
||||
String errorUrl = base + "/api/wave-redirect/error?ref=" + intention.getId();
|
||||
|
||||
WaveCheckoutSessionResponse session;
|
||||
try {
|
||||
session = waveCheckoutService.createSession(
|
||||
montant.toString(), "XOF", successUrl, errorUrl,
|
||||
intention.getId().toString(), toE164(request.numeroTelephone()));
|
||||
} catch (WaveCheckoutException e) {
|
||||
LOG.errorf(e, "Wave Checkout (dépôt épargne) : %s", e.getMessage());
|
||||
intention.setStatut(StatutIntentionPaiement.ECHOUEE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
throw new jakarta.ws.rs.BadRequestException("Wave : " + e.getMessage());
|
||||
}
|
||||
|
||||
intention.setWaveCheckoutSessionId(session.id);
|
||||
intention.setWaveLaunchUrl(session.waveLaunchUrl);
|
||||
intention.setStatut(StatutIntentionPaiement.EN_COURS);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
LOG.infof("Dépôt épargne Wave initié : intention=%s, compte=%s",
|
||||
intention.getId(), request.compteId());
|
||||
|
||||
return VersementGatewayResponse.builder()
|
||||
.versementId(intention.getId())
|
||||
.waveLaunchUrl(session.waveLaunchUrl)
|
||||
.waveCheckoutSessionId(session.id)
|
||||
.clientReference(intention.getId().toString())
|
||||
.montant(montant)
|
||||
.statut("EN_ATTENTE")
|
||||
.message("Ouvrez Wave pour confirmer le dépôt, puis vous serez renvoyé dans UnionFlow.")
|
||||
.build();
|
||||
}
|
||||
|
||||
// ── Polling statut ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Vérifie le statut d'une intention Wave.
|
||||
* Utilisé par le deep link de retour (mobile) et le polling web.
|
||||
*/
|
||||
@Transactional
|
||||
public VersementStatutResponse verifierStatutVersement(UUID intentionId) {
|
||||
IntentionPaiement intention = intentionPaiementRepository.findById(intentionId);
|
||||
if (intention == null) {
|
||||
throw new NotFoundException("Intention non trouvée : " + intentionId);
|
||||
}
|
||||
|
||||
if (intention.isCompletee()) {
|
||||
return buildStatutResponse(intention, "Versement confirmé !");
|
||||
}
|
||||
if (StatutIntentionPaiement.EXPIREE.equals(intention.getStatut())
|
||||
|| StatutIntentionPaiement.ECHOUEE.equals(intention.getStatut())) {
|
||||
return buildStatutResponse(intention,
|
||||
"Versement " + intention.getStatut().name().toLowerCase());
|
||||
}
|
||||
if (intention.isExpiree()) {
|
||||
intention.setStatut(StatutIntentionPaiement.EXPIREE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
return buildStatutResponse(intention, "Session expirée, veuillez recommencer");
|
||||
}
|
||||
|
||||
if (intention.getWaveCheckoutSessionId() != null) {
|
||||
try {
|
||||
WaveCheckoutService.WaveSessionStatusResponse waveStatus =
|
||||
waveCheckoutService.getSession(intention.getWaveCheckoutSessionId());
|
||||
if (waveStatus.isSucceeded()) {
|
||||
confirmerVersementWave(intention, waveStatus.transactionId);
|
||||
return buildStatutResponse(intention, "Versement confirmé !");
|
||||
} else if (waveStatus.isExpired()) {
|
||||
intention.setStatut(StatutIntentionPaiement.EXPIREE);
|
||||
intentionPaiementRepository.persist(intention);
|
||||
return buildStatutResponse(intention, "Session Wave expirée");
|
||||
}
|
||||
} catch (WaveCheckoutService.WaveCheckoutException e) {
|
||||
LOG.warnf(e, "Impossible de vérifier la session Wave %s — retry au prochain appel",
|
||||
intention.getWaveCheckoutSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
return buildStatutResponse(intention, "En attente de confirmation Wave...");
|
||||
}
|
||||
|
||||
// ── Flux manuel ───────────────────────────────────────────────────────────
|
||||
|
||||
@Transactional
|
||||
public VersementResponse declarerVersementManuel(DeclarerVersementManuelRequest request) {
|
||||
Membre membreConnecte = getMembreConnecte();
|
||||
LOG.infof("Déclaration versement manuel — membre %s, cotisation %s, méthode %s",
|
||||
membreConnecte.getNumeroMembre(), request.cotisationId(), request.methodePaiement());
|
||||
|
||||
Cotisation cotisation = versementRepository.getEntityManager()
|
||||
.createQuery("SELECT c FROM Cotisation c WHERE c.id = :id", Cotisation.class)
|
||||
.setParameter("id", request.cotisationId())
|
||||
.getResultList().stream().findFirst()
|
||||
.orElseThrow(() -> new NotFoundException("Cotisation non trouvée : " + request.cotisationId()));
|
||||
|
||||
if (!cotisation.getMembre().getId().equals(membreConnecte.getId())) {
|
||||
throw new IllegalArgumentException("Cette cotisation n'appartient pas au membre connecté");
|
||||
}
|
||||
|
||||
Versement versement = new Versement();
|
||||
versement.setNumeroReference("VRS-MAN-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
|
||||
versement.setMontant(cotisation.getMontantDu());
|
||||
versement.setCodeDevise("XOF");
|
||||
versement.setMethodePaiement(request.methodePaiement());
|
||||
versement.setStatutPaiement("EN_ATTENTE_VALIDATION");
|
||||
versement.setMembre(membreConnecte);
|
||||
versement.setReferenceExterne(request.reference());
|
||||
versement.setCommentaire(request.commentaire());
|
||||
versement.setDatePaiement(LocalDateTime.now());
|
||||
versement.setCreePar(membreConnecte.getEmail());
|
||||
versementRepository.persist(versement);
|
||||
|
||||
// Notifier le trésorier
|
||||
membreOrganisationRepository.findFirstByMembreId(membreConnecte.getId())
|
||||
.ifPresent(mo -> {
|
||||
CreateNotificationRequest notif = CreateNotificationRequest.builder()
|
||||
.typeNotification("VALIDATION_VERSEMENT_REQUIS")
|
||||
.priorite("HAUTE")
|
||||
.sujet("Validation versement manuel requis")
|
||||
.corps("Le membre " + membreConnecte.getNumeroMembre()
|
||||
+ " a déclaré un versement manuel de " + versement.getMontant()
|
||||
+ " XOF (réf: " + versement.getNumeroReference() + ") à valider.")
|
||||
.organisationId(mo.getOrganisation().getId())
|
||||
.build();
|
||||
notificationService.creerNotification(notif);
|
||||
});
|
||||
|
||||
LOG.infof("Versement manuel déclaré : %s (EN_ATTENTE_VALIDATION)", versement.getNumeroReference());
|
||||
return convertToResponse(versement);
|
||||
}
|
||||
|
||||
// ── Réconciliation Wave (appelé par WaveRedirectResource) ─────────────────
|
||||
|
||||
/**
|
||||
* Marque l'intention COMPLETEE et met à jour les cotisations cibles.
|
||||
* Idempotent : si déjà complétée, retourne sans effet.
|
||||
*/
|
||||
@Transactional
|
||||
public void confirmerVersementWave(IntentionPaiement intention, String waveTransactionId) {
|
||||
if (intention.isCompletee()) return;
|
||||
|
||||
intention.setStatut(StatutIntentionPaiement.COMPLETEE);
|
||||
intention.setDateCompletion(LocalDateTime.now());
|
||||
if (waveTransactionId != null) {
|
||||
intention.setWaveTransactionId(waveTransactionId);
|
||||
}
|
||||
intentionPaiementRepository.persist(intention);
|
||||
|
||||
String objetsCibles = intention.getObjetsCibles();
|
||||
if (objetsCibles == null || objetsCibles.isBlank()) return;
|
||||
|
||||
try {
|
||||
com.fasterxml.jackson.databind.JsonNode arr =
|
||||
new com.fasterxml.jackson.databind.ObjectMapper().readTree(objetsCibles);
|
||||
if (!arr.isArray()) return;
|
||||
|
||||
for (com.fasterxml.jackson.databind.JsonNode node : arr) {
|
||||
if (!"COTISATION".equals(node.path("type").asText())) continue;
|
||||
|
||||
UUID cotisationId = UUID.fromString(node.get("id").asText());
|
||||
BigDecimal montant = node.has("montant")
|
||||
? new BigDecimal(node.get("montant").asText())
|
||||
: intention.getMontantTotal();
|
||||
|
||||
Cotisation cotisation = versementRepository.getEntityManager()
|
||||
.find(Cotisation.class, cotisationId);
|
||||
if (cotisation == null) continue;
|
||||
|
||||
cotisation.setMontantPaye(montant);
|
||||
cotisation.setStatut("PAYEE");
|
||||
cotisation.setDatePaiement(LocalDateTime.now());
|
||||
versementRepository.getEntityManager().merge(cotisation);
|
||||
LOG.infof("Cotisation %s marquée PAYEE — Wave txn %s",
|
||||
cotisationId, waveTransactionId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.errorf(e, "Erreur réconciliation cotisations pour intention %s", intention.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Méthodes privées ──────────────────────────────────────────────────────
|
||||
|
||||
private Membre getMembreConnecte() {
|
||||
String email = securityIdentity.getPrincipal().getName();
|
||||
return membreRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new NotFoundException(
|
||||
"Membre non trouvé pour l'email : " + email));
|
||||
}
|
||||
|
||||
private VersementResponse convertToResponse(Versement v) {
|
||||
VersementResponse r = new VersementResponse();
|
||||
r.setId(v.getId());
|
||||
r.setNumeroReference(v.getNumeroReference());
|
||||
r.setMontant(v.getMontant());
|
||||
r.setCodeDevise(v.getCodeDevise());
|
||||
r.setMethodePaiement(v.getMethodePaiement());
|
||||
r.setStatutPaiement(v.getStatutPaiement());
|
||||
r.setDatePaiement(v.getDatePaiement());
|
||||
r.setDateValidation(v.getDateValidation());
|
||||
r.setValidateur(v.getValidateur());
|
||||
r.setReferenceExterne(v.getReferenceExterne());
|
||||
r.setUrlPreuve(v.getUrlPreuve());
|
||||
r.setCommentaire(v.getCommentaire());
|
||||
r.setNumeroTelephone(v.getNumeroTelephone());
|
||||
if (v.getMembre() != null) {
|
||||
r.setMembreId(v.getMembre().getId());
|
||||
}
|
||||
if (v.getTransactionWave() != null) {
|
||||
r.setTransactionWaveId(v.getTransactionWave().getId());
|
||||
}
|
||||
r.setDateCreation(v.getDateCreation());
|
||||
r.setDateModification(v.getDateModification());
|
||||
r.setActif(v.getActif());
|
||||
enrichirLibelles(v, r);
|
||||
return r;
|
||||
}
|
||||
|
||||
private VersementSummaryResponse convertToSummaryResponse(Versement v) {
|
||||
if (v == null) return null;
|
||||
return new VersementSummaryResponse(
|
||||
v.getId(),
|
||||
v.getNumeroReference(),
|
||||
v.getMontant(),
|
||||
v.getCodeDevise(),
|
||||
resolveLibelle("METHODE_PAIEMENT", v.getMethodePaiement()),
|
||||
v.getStatutPaiement(),
|
||||
resolveLibelle("STATUT_PAIEMENT", v.getStatutPaiement()),
|
||||
resolveSeverity("STATUT_PAIEMENT", v.getStatutPaiement()),
|
||||
v.getDatePaiement());
|
||||
}
|
||||
|
||||
private void enrichirLibelles(Versement v, VersementResponse r) {
|
||||
r.setMethodePaiementLibelle(resolveLibelle("METHODE_PAIEMENT", v.getMethodePaiement()));
|
||||
r.setStatutPaiementLibelle(resolveLibelle("STATUT_PAIEMENT", v.getStatutPaiement()));
|
||||
r.setStatutPaiementSeverity(resolveSeverity("STATUT_PAIEMENT", v.getStatutPaiement()));
|
||||
}
|
||||
|
||||
private String resolveLibelle(String domaine, String code) {
|
||||
if (code == null) return null;
|
||||
return typeReferenceRepository.findByDomaineAndCode(domaine, code)
|
||||
.map(dev.lions.unionflow.server.entity.TypeReference::getLibelle)
|
||||
.orElse(code);
|
||||
}
|
||||
|
||||
private String resolveSeverity(String domaine, String code) {
|
||||
if (code == null) return null;
|
||||
return typeReferenceRepository.findByDomaineAndCode(domaine, code)
|
||||
.map(dev.lions.unionflow.server.entity.TypeReference::getSeverity)
|
||||
.orElse("info");
|
||||
}
|
||||
|
||||
private VersementStatutResponse buildStatutResponse(IntentionPaiement intention, String message) {
|
||||
return VersementStatutResponse.builder()
|
||||
.intentionId(intention.getId())
|
||||
.statut(intention.getStatut().name())
|
||||
.confirme(intention.isCompletee())
|
||||
.waveLaunchUrl(intention.getWaveLaunchUrl())
|
||||
.waveCheckoutSessionId(intention.getWaveCheckoutSessionId())
|
||||
.waveTransactionId(intention.getWaveTransactionId())
|
||||
.montant(intention.getMontantTotal())
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Format E.164 pour Wave : 771234567 → +221771234567 */
|
||||
static String toE164(String numeroTelephone) {
|
||||
if (numeroTelephone == null || numeroTelephone.isBlank()) return null;
|
||||
String digits = numeroTelephone.replaceAll("\\D", "");
|
||||
if (digits.length() == 9 && (digits.startsWith("7") || digits.startsWith("0"))) {
|
||||
return "+221" + (digits.startsWith("0") ? digits.substring(1) : digits);
|
||||
}
|
||||
if (digits.length() >= 9 && digits.startsWith("221")) return "+" + digits;
|
||||
return numeroTelephone.startsWith("+") ? numeroTelephone : "+" + digits;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,19 @@ quarkus.application.version=1.0.0
|
||||
# Backup configuration
|
||||
unionflow.backup.directory=${BACKUP_DIR:/tmp/unionflow-backups}
|
||||
|
||||
# Keycloak Admin API (pour la déconnexion globale des sessions)
|
||||
keycloak.admin.url=${KEYCLOAK_URL:http://localhost:8180}
|
||||
keycloak.admin.username=${KEYCLOAK_ADMIN_USERNAME:admin}
|
||||
keycloak.admin.password=${KEYCLOAK_ADMIN_PASSWORD:admin}
|
||||
keycloak.admin.realm=${KEYCLOAK_REALM:unionflow}
|
||||
|
||||
# Vérification des mises à jour disponibles (propriété optionnelle — Optional<String> côté Java)
|
||||
# Absent par défaut : le check distant est désactivé, la version courante est retournée honnêtement.
|
||||
# Pour activer : définir UNIONFLOW_UPDATES_CHECK_URL (convention MicroProfile env var) ou
|
||||
# ajouter la ligne suivante dans application-prod.properties :
|
||||
# unionflow.updates.check-url=https://releases.lions.dev/unionflow/latest.json
|
||||
# L'endpoint doit retourner un JSON {"version": "x.y.z"}
|
||||
|
||||
# Jackson — sérialisation des dates en ISO string (pas en tableau [year, month, day])
|
||||
quarkus.jackson.write-dates-as-timestamps=false
|
||||
quarkus.jackson.serialization-inclusion=non_null
|
||||
@@ -172,3 +185,15 @@ mp.messaging.incoming.contributions-events-in.topic=unionflow.contributions.even
|
||||
mp.messaging.incoming.contributions-events-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.contributions-events-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.contributions-events-in.group.id=unionflow-websocket-server
|
||||
|
||||
# Chat Messages — Messagerie instantanée
|
||||
mp.messaging.outgoing.chat-messages-out.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.chat-messages-out.topic=unionflow.chat.messages
|
||||
mp.messaging.outgoing.chat-messages-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.chat-messages-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
|
||||
mp.messaging.incoming.chat-messages-in.connector=smallrye-kafka
|
||||
mp.messaging.incoming.chat-messages-in.topic=unionflow.chat.messages
|
||||
mp.messaging.incoming.chat-messages-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.chat-messages-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.chat-messages-in.group.id=unionflow-websocket-server
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- V25 : Rendre numero_transaction nullable dans la table paiements
|
||||
--
|
||||
-- Problème : la colonne numero_transaction est définie NOT NULL dans V1,
|
||||
-- mais l'entité Paiement ne la mappe pas. Un paiement Wave créé en état
|
||||
-- EN_ATTENTE n'a pas encore de numéro de transaction (celui-ci arrive via
|
||||
-- le callback Wave après complétion). La contrainte NOT NULL empêche tout
|
||||
-- INSERT et provoque un 500.
|
||||
--
|
||||
-- La colonne transaction_wave_id stocke déjà le session ID Wave ;
|
||||
-- numero_transaction est distinct (ID de transaction finalisée chez Wave).
|
||||
|
||||
ALTER TABLE paiements
|
||||
ALTER COLUMN numero_transaction DROP NOT NULL;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- V26 : Correction des colonnes legacy NOT NULL non mappées dans l'entité Paiement
|
||||
--
|
||||
-- Contexte : La table paiements a été créée par V1 avec des colonnes NOT NULL
|
||||
-- (statut, type_paiement). Hibernate en mode "update" a ensuite ajouté les colonnes
|
||||
-- métier réelles (statut_paiement, methode_paiement...) sans supprimer les anciennes.
|
||||
-- Résultat : l'entité Paiement ne mappe pas ces colonnes V1, et tout INSERT échoue.
|
||||
--
|
||||
-- Solutions :
|
||||
-- - type_paiement : équivalent fonctionnel de methode_paiement → nullable
|
||||
-- - statut : remplacé par statut_paiement dans l'entité → nullable
|
||||
|
||||
ALTER TABLE paiements
|
||||
ALTER COLUMN type_paiement DROP NOT NULL;
|
||||
|
||||
ALTER TABLE paiements
|
||||
ALTER COLUMN statut DROP NOT NULL;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- V27 : Ajoute la colonne numero_telephone dans la table paiements (versements)
|
||||
--
|
||||
-- Contexte : La refonte conceptuelle remplace "Paiement" par "Versement".
|
||||
-- L'application Wave est installée sur le même téléphone qu'UnionFlow ;
|
||||
-- le numéro de téléphone du membre est envoyé automatiquement depuis son profil
|
||||
-- afin de pré-remplir le formulaire Wave (deep link natif).
|
||||
-- Ce champ mémorise le numéro utilisé lors du versement Wave.
|
||||
|
||||
ALTER TABLE paiements
|
||||
ADD COLUMN IF NOT EXISTS numero_telephone VARCHAR(20);
|
||||
@@ -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);
|
||||
@@ -0,0 +1,26 @@
|
||||
-- V29: Table de persistance de la configuration système (maintenance_mode, scheduled_maintenance, etc.)
|
||||
-- Remplace le stockage en RAM (AtomicReference) pour les paramètres critiques.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS system_config (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_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,
|
||||
config_key VARCHAR(100) NOT NULL,
|
||||
config_value TEXT,
|
||||
CONSTRAINT uk_system_config_key UNIQUE (config_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_system_config_key ON system_config (config_key);
|
||||
|
||||
-- Valeurs initiales
|
||||
INSERT INTO system_config (config_key, config_value, cree_par) VALUES
|
||||
('maintenance_mode', 'false', 'SYSTEM'),
|
||||
('maintenance_emergency', 'false', 'SYSTEM'),
|
||||
('scheduled_maintenance_at', NULL, 'SYSTEM'),
|
||||
('scheduled_maintenance_reason', NULL, 'SYSTEM'),
|
||||
('scheduled_maintenance_status', 'NONE', 'SYSTEM')
|
||||
ON CONFLICT (config_key) DO NOTHING;
|
||||
@@ -0,0 +1,109 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("ContactPolicy")
|
||||
class ContactPolicyTest {
|
||||
|
||||
private static Organisation newOrg() {
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Org Test");
|
||||
return org;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getters/setters — tous les champs")
|
||||
void gettersSetters() {
|
||||
ContactPolicy policy = new ContactPolicy();
|
||||
Organisation org = newOrg();
|
||||
policy.setOrganisation(org);
|
||||
policy.setTypePolitique(TypePolitiqueCommunication.BUREAU_SEULEMENT);
|
||||
policy.setAutoriserMembreVersMembre(false);
|
||||
policy.setAutoriserMembreVersRole(true);
|
||||
policy.setAutoriserNotesVocales(false);
|
||||
|
||||
assertThat(policy.getOrganisation()).isEqualTo(org);
|
||||
assertThat(policy.getTypePolitique()).isEqualTo(TypePolitiqueCommunication.BUREAU_SEULEMENT);
|
||||
assertThat(policy.getAutoriserMembreVersMembre()).isFalse();
|
||||
assertThat(policy.getAutoriserMembreVersRole()).isTrue();
|
||||
assertThat(policy.getAutoriserNotesVocales()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate — valeurs par défaut : OUVERT, tout à true")
|
||||
void onCreate_defaults() {
|
||||
ContactPolicy policy = new ContactPolicy();
|
||||
policy.setOrganisation(newOrg());
|
||||
|
||||
policy.onCreate();
|
||||
|
||||
assertThat(policy.getTypePolitique()).isEqualTo(TypePolitiqueCommunication.OUVERT);
|
||||
assertThat(policy.getAutoriserMembreVersMembre()).isTrue();
|
||||
assertThat(policy.getAutoriserMembreVersRole()).isTrue();
|
||||
assertThat(policy.getAutoriserNotesVocales()).isTrue();
|
||||
assertThat(policy.getDateCreation()).isNotNull();
|
||||
assertThat(policy.getActif()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate — ne remplace pas les valeurs existantes")
|
||||
void onCreate_preservesExisting() {
|
||||
ContactPolicy policy = new ContactPolicy();
|
||||
policy.setOrganisation(newOrg());
|
||||
policy.setTypePolitique(TypePolitiqueCommunication.GROUPES_INTERNES);
|
||||
policy.setAutoriserNotesVocales(false);
|
||||
|
||||
policy.onCreate();
|
||||
|
||||
assertThat(policy.getTypePolitique()).isEqualTo(TypePolitiqueCommunication.GROUPES_INTERNES);
|
||||
assertThat(policy.getAutoriserNotesVocales()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("builder — tous les champs")
|
||||
void builder_allFields() {
|
||||
Organisation org = newOrg();
|
||||
ContactPolicy policy = ContactPolicy.builder()
|
||||
.organisation(org)
|
||||
.typePolitique(TypePolitiqueCommunication.OUVERT)
|
||||
.autoriserMembreVersMembre(true)
|
||||
.autoriserMembreVersRole(true)
|
||||
.autoriserNotesVocales(true)
|
||||
.build();
|
||||
|
||||
assertThat(policy.getOrganisation()).isEqualTo(org);
|
||||
assertThat(policy.getTypePolitique()).isEqualTo(TypePolitiqueCommunication.OUVERT);
|
||||
assertThat(policy.getAutoriserNotesVocales()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("equals et hashCode")
|
||||
void equalsHashCode() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Organisation org = newOrg();
|
||||
|
||||
ContactPolicy a = ContactPolicy.builder().organisation(org)
|
||||
.typePolitique(TypePolitiqueCommunication.OUVERT).build();
|
||||
a.setId(id);
|
||||
ContactPolicy b = ContactPolicy.builder().organisation(org)
|
||||
.typePolitique(TypePolitiqueCommunication.OUVERT).build();
|
||||
b.setId(id);
|
||||
|
||||
assertThat(a).isEqualTo(b);
|
||||
assertThat(a.hashCode()).isEqualTo(b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toString non null")
|
||||
void toString_nonNull() {
|
||||
ContactPolicy policy = ContactPolicy.builder().organisation(newOrg()).build();
|
||||
assertThat(policy.toString()).isNotNull().isNotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("ConversationParticipant")
|
||||
class ConversationParticipantTest {
|
||||
|
||||
private static Membre newMembre() {
|
||||
Membre m = new Membre();
|
||||
m.setId(UUID.randomUUID());
|
||||
m.setNumeroMembre("M1");
|
||||
m.setPrenom("Alpha");
|
||||
m.setNom("Diallo");
|
||||
m.setEmail("alpha@test.com");
|
||||
m.setDateNaissance(LocalDate.now());
|
||||
return m;
|
||||
}
|
||||
|
||||
private static Conversation newConversation() {
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Org Test");
|
||||
Conversation c = new Conversation();
|
||||
c.setId(UUID.randomUUID());
|
||||
c.setOrganisation(org);
|
||||
c.setTypeConversation(TypeConversation.DIRECTE);
|
||||
c.setStatut(StatutConversation.ACTIVE);
|
||||
c.setNombreMessages(0);
|
||||
return c;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getters/setters")
|
||||
void gettersSetters() {
|
||||
ConversationParticipant p = new ConversationParticipant();
|
||||
Conversation conv = newConversation();
|
||||
Membre membre = newMembre();
|
||||
p.setConversation(conv);
|
||||
p.setMembre(membre);
|
||||
p.setRoleDansConversation("INITIATEUR");
|
||||
p.setNotifier(true);
|
||||
|
||||
assertThat(p.getConversation()).isEqualTo(conv);
|
||||
assertThat(p.getMembre()).isEqualTo(membre);
|
||||
assertThat(p.getRoleDansConversation()).isEqualTo("INITIATEUR");
|
||||
assertThat(p.getNotifier()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("estInitiateur — INITIATEUR → true")
|
||||
void estInitiateur_true() {
|
||||
ConversationParticipant p = buildMinimal("INITIATEUR");
|
||||
assertThat(p.estInitiateur()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("estInitiateur — PARTICIPANT → false")
|
||||
void estInitiateur_false() {
|
||||
ConversationParticipant p = buildMinimal("PARTICIPANT");
|
||||
assertThat(p.estInitiateur()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("marquerLu — luJusqua mis à jour")
|
||||
void marquerLu() {
|
||||
ConversationParticipant p = buildMinimal("PARTICIPANT");
|
||||
p.setLuJusqua(null);
|
||||
p.marquerLu();
|
||||
assertThat(p.getLuJusqua()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("marquerLu — luJusqua remplace l'ancien")
|
||||
void marquerLu_replaceOld() {
|
||||
ConversationParticipant p = buildMinimal("PARTICIPANT");
|
||||
LocalDateTime old = LocalDateTime.now().minusDays(1);
|
||||
p.setLuJusqua(old);
|
||||
p.marquerLu();
|
||||
assertThat(p.getLuJusqua()).isAfter(old);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("builder — valeurs par défaut")
|
||||
void builder_defaults() {
|
||||
ConversationParticipant p = ConversationParticipant.builder()
|
||||
.conversation(newConversation())
|
||||
.membre(newMembre())
|
||||
.build();
|
||||
assertThat(p.getRoleDansConversation()).isEqualTo("PARTICIPANT");
|
||||
assertThat(p.getNotifier()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("equals et hashCode")
|
||||
void equalsHashCode() {
|
||||
UUID id = UUID.randomUUID();
|
||||
ConversationParticipant a = buildMinimal("PARTICIPANT");
|
||||
a.setId(id);
|
||||
ConversationParticipant b = buildMinimal("PARTICIPANT");
|
||||
b.setId(id);
|
||||
assertThat(a).isEqualTo(b);
|
||||
assertThat(a.hashCode()).isEqualTo(b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toString non null")
|
||||
void toString_nonNull() {
|
||||
assertThat(buildMinimal("PARTICIPANT").toString()).isNotNull().isNotEmpty();
|
||||
}
|
||||
|
||||
private ConversationParticipant buildMinimal(String role) {
|
||||
ConversationParticipant p = new ConversationParticipant();
|
||||
p.setConversation(newConversation());
|
||||
p.setMembre(newMembre());
|
||||
p.setRoleDansConversation(role);
|
||||
p.setNotifier(true);
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +1,166 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.communication.ConversationType;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
@DisplayName("Conversation")
|
||||
class ConversationTest {
|
||||
|
||||
private static Membre newMembre() {
|
||||
Membre m = new Membre();
|
||||
m.setId(UUID.randomUUID());
|
||||
m.setNumeroMembre("M1");
|
||||
m.setPrenom("Alpha");
|
||||
m.setNom("Diallo");
|
||||
m.setEmail("alpha@test.com");
|
||||
m.setDateNaissance(LocalDate.now());
|
||||
return m;
|
||||
}
|
||||
|
||||
private static Organisation newOrg() {
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Tontine Test");
|
||||
return org;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getters/setters de base")
|
||||
@DisplayName("getters/setters — tous les champs")
|
||||
void gettersSetters() {
|
||||
Conversation c = new Conversation();
|
||||
c.setName("Groupe Test");
|
||||
c.setDescription("Description groupe");
|
||||
c.setType(ConversationType.GROUP);
|
||||
c.setIsMuted(false);
|
||||
c.setIsPinned(true);
|
||||
c.setIsArchived(false);
|
||||
Conversation conv = new Conversation();
|
||||
Organisation org = newOrg();
|
||||
conv.setOrganisation(org);
|
||||
conv.setTypeConversation(TypeConversation.DIRECTE);
|
||||
conv.setStatut(StatutConversation.ACTIVE);
|
||||
conv.setTitre("Discussion");
|
||||
conv.setNombreMessages(5);
|
||||
|
||||
assertThat(c.getName()).isEqualTo("Groupe Test");
|
||||
assertThat(c.getDescription()).isEqualTo("Description groupe");
|
||||
assertThat(c.getType()).isEqualTo(ConversationType.GROUP);
|
||||
assertThat(c.getIsMuted()).isFalse();
|
||||
assertThat(c.getIsPinned()).isTrue();
|
||||
assertThat(c.getIsArchived()).isFalse();
|
||||
assertThat(conv.getOrganisation()).isEqualTo(org);
|
||||
assertThat(conv.getTypeConversation()).isEqualTo(TypeConversation.DIRECTE);
|
||||
assertThat(conv.getStatut()).isEqualTo(StatutConversation.ACTIVE);
|
||||
assertThat(conv.getTitre()).isEqualTo("Discussion");
|
||||
assertThat(conv.getNombreMessages()).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onUpdate (PreUpdate) - met à jour updatedAt via réflexion")
|
||||
void onUpdate_setsUpdatedAt() throws Exception {
|
||||
Conversation c = new Conversation();
|
||||
assertThat(c.getUpdatedAt()).isNull();
|
||||
|
||||
Method onUpdate = Conversation.class.getDeclaredMethod("onUpdate");
|
||||
onUpdate.setAccessible(true);
|
||||
|
||||
LocalDateTime before = LocalDateTime.now().minusSeconds(1);
|
||||
onUpdate.invoke(c);
|
||||
LocalDateTime after = LocalDateTime.now().plusSeconds(1);
|
||||
|
||||
assertThat(c.getUpdatedAt()).isNotNull();
|
||||
assertThat(c.getUpdatedAt()).isAfter(before);
|
||||
assertThat(c.getUpdatedAt()).isBefore(after);
|
||||
@DisplayName("estActive — ACTIVE → true")
|
||||
void estActive_active() {
|
||||
Conversation conv = buildMinimal();
|
||||
conv.setStatut(StatutConversation.ACTIVE);
|
||||
assertThat(conv.estActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onUpdate appelé deux fois met à jour updatedAt à chaque fois")
|
||||
void onUpdate_calledTwice_updatesEachTime() throws Exception {
|
||||
Conversation c = new Conversation();
|
||||
|
||||
Method onUpdate = Conversation.class.getDeclaredMethod("onUpdate");
|
||||
onUpdate.setAccessible(true);
|
||||
|
||||
onUpdate.invoke(c);
|
||||
LocalDateTime first = c.getUpdatedAt();
|
||||
|
||||
// petit délai pour différencier les timestamps
|
||||
Thread.sleep(5);
|
||||
|
||||
onUpdate.invoke(c);
|
||||
LocalDateTime second = c.getUpdatedAt();
|
||||
|
||||
assertThat(second).isAfterOrEqualTo(first);
|
||||
@DisplayName("estActive — ARCHIVEE → false")
|
||||
void estActive_archivee() {
|
||||
Conversation conv = buildMinimal();
|
||||
conv.setStatut(StatutConversation.ARCHIVEE);
|
||||
assertThat(conv.estActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("participants initialisé à liste vide")
|
||||
@DisplayName("archiver — passe le statut à ARCHIVEE")
|
||||
void archiver() {
|
||||
Conversation conv = buildMinimal();
|
||||
conv.setStatut(StatutConversation.ACTIVE);
|
||||
conv.archiver();
|
||||
assertThat(conv.getStatut()).isEqualTo(StatutConversation.ARCHIVEE);
|
||||
assertThat(conv.estActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("enregistrerNouveauMessage — incrémente le compteur et met à jour la date")
|
||||
void enregistrerNouveauMessage() {
|
||||
Conversation conv = buildMinimal();
|
||||
conv.setNombreMessages(3);
|
||||
conv.enregistrerNouveauMessage();
|
||||
assertThat(conv.getNombreMessages()).isEqualTo(4);
|
||||
assertThat(conv.getDernierMessageAt()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("enregistrerNouveauMessage — part de null")
|
||||
void enregistrerNouveauMessage_partDeNull() {
|
||||
Conversation conv = buildMinimal();
|
||||
conv.setNombreMessages(null);
|
||||
conv.enregistrerNouveauMessage();
|
||||
assertThat(conv.getNombreMessages()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate — initialise statut et nombreMessages si null")
|
||||
void onCreate_initDefaults() {
|
||||
Conversation conv = new Conversation();
|
||||
conv.setOrganisation(newOrg());
|
||||
conv.setTypeConversation(TypeConversation.DIRECTE);
|
||||
|
||||
conv.onCreate();
|
||||
|
||||
assertThat(conv.getStatut()).isEqualTo(StatutConversation.ACTIVE);
|
||||
assertThat(conv.getNombreMessages()).isEqualTo(0);
|
||||
assertThat(conv.getDateCreation()).isNotNull();
|
||||
assertThat(conv.getActif()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ROLE_CANAL — roleCible renseigné")
|
||||
void roleCanalType() {
|
||||
Conversation conv = buildMinimal();
|
||||
conv.setTypeConversation(TypeConversation.ROLE_CANAL);
|
||||
conv.setRoleCible("TRESORIER");
|
||||
conv.setTitre("Trésorier");
|
||||
|
||||
assertThat(conv.getTypeConversation()).isEqualTo(TypeConversation.ROLE_CANAL);
|
||||
assertThat(conv.getRoleCible()).isEqualTo("TRESORIER");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("participants initialisé à liste vide (builder default)")
|
||||
void participants_initializedEmpty() {
|
||||
Conversation c = new Conversation();
|
||||
assertThat(c.getParticipants()).isNotNull().isEmpty();
|
||||
Conversation conv = Conversation.builder()
|
||||
.organisation(newOrg())
|
||||
.typeConversation(TypeConversation.DIRECTE)
|
||||
.build();
|
||||
assertThat(conv.getParticipants()).isNotNull().isEmpty();
|
||||
assertThat(conv.getMessages()).isNotNull().isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("messages initialisé à liste vide")
|
||||
void messages_initializedEmpty() {
|
||||
Conversation c = new Conversation();
|
||||
assertThat(c.getMessages()).isNotNull().isEmpty();
|
||||
@DisplayName("equals et hashCode")
|
||||
void equalsHashCode() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Organisation org = newOrg();
|
||||
|
||||
Conversation a = buildMinimal();
|
||||
a.setId(id);
|
||||
a.setOrganisation(org);
|
||||
Conversation b = buildMinimal();
|
||||
b.setId(id);
|
||||
b.setOrganisation(org);
|
||||
|
||||
assertThat(a).isEqualTo(b);
|
||||
assertThat(a.hashCode()).isEqualTo(b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isMuted et isPinned et isArchived défaut false")
|
||||
void defaultFlags_areFalse() {
|
||||
Conversation c = new Conversation();
|
||||
assertThat(c.getIsMuted()).isFalse();
|
||||
assertThat(c.getIsPinned()).isFalse();
|
||||
assertThat(c.getIsArchived()).isFalse();
|
||||
@DisplayName("toString non null")
|
||||
void toString_nonNull() {
|
||||
assertThat(buildMinimal().toString()).isNotNull().isNotEmpty();
|
||||
}
|
||||
|
||||
private Conversation buildMinimal() {
|
||||
Conversation conv = new Conversation();
|
||||
conv.setOrganisation(newOrg());
|
||||
conv.setTypeConversation(TypeConversation.DIRECTE);
|
||||
conv.setStatut(StatutConversation.ACTIVE);
|
||||
conv.setNombreMessages(0);
|
||||
return conv;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import dev.lions.unionflow.server.api.enums.membre.StatutMembre;
|
||||
import dev.lions.unionflow.server.api.enums.solidarite.StatutAide;
|
||||
import dev.lions.unionflow.server.api.enums.solidarite.TypeAide;
|
||||
import dev.lions.unionflow.server.api.enums.wave.StatutWebhook;
|
||||
import dev.lions.unionflow.server.api.enums.communication.ConversationType;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -134,38 +135,85 @@ class EntityCoverageTest {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Conversation — onUpdate ─────────────────────────────────────────────
|
||||
// ─── Conversation v4 ────────────────────────────────────────────────────
|
||||
|
||||
@Nested
|
||||
@DisplayName("Conversation — onUpdate() non couvert")
|
||||
@DisplayName("Conversation v4 — méthodes métier")
|
||||
class ConversationCoverage {
|
||||
|
||||
@Test
|
||||
@DisplayName("Conversation getters/setters de base")
|
||||
@DisplayName("Conversation getters/setters v4")
|
||||
void gettersSetters() {
|
||||
Conversation c = new Conversation();
|
||||
c.setName("Chat Test");
|
||||
c.setDescription("Description");
|
||||
c.setType(ConversationType.GROUP);
|
||||
c.setIsMuted(false);
|
||||
c.setIsPinned(true);
|
||||
c.setIsArchived(false);
|
||||
c.setUpdatedAt(LocalDateTime.now());
|
||||
c.setTypeConversation(TypeConversation.DIRECTE);
|
||||
c.setRoleCible("TRESORIER");
|
||||
c.setTitre("Canal Trésorier");
|
||||
c.setStatut(StatutConversation.ACTIVE);
|
||||
c.setDernierMessageAt(LocalDateTime.now());
|
||||
c.setNombreMessages(3);
|
||||
|
||||
assertThat(c.getName()).isEqualTo("Chat Test");
|
||||
assertThat(c.getType()).isEqualTo(ConversationType.GROUP);
|
||||
assertThat(c.getIsPinned()).isTrue();
|
||||
assertThat(c.getTypeConversation()).isEqualTo(TypeConversation.DIRECTE);
|
||||
assertThat(c.getRoleCible()).isEqualTo("TRESORIER");
|
||||
assertThat(c.getTitre()).isEqualTo("Canal Trésorier");
|
||||
assertThat(c.getStatut()).isEqualTo(StatutConversation.ACTIVE);
|
||||
assertThat(c.getNombreMessages()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onUpdate() met à jour updatedAt")
|
||||
void onUpdate_setsUpdatedAt() {
|
||||
@DisplayName("estActive() — ACTIVE → true, ARCHIVEE → false")
|
||||
void estActive() {
|
||||
Conversation active = new Conversation();
|
||||
active.setStatut(StatutConversation.ACTIVE);
|
||||
assertThat(active.estActive()).isTrue();
|
||||
|
||||
Conversation archivee = new Conversation();
|
||||
archivee.setStatut(StatutConversation.ARCHIVEE);
|
||||
assertThat(archivee.estActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("archiver() — passe à ARCHIVEE")
|
||||
void archiver() {
|
||||
Conversation c = new Conversation();
|
||||
c.setName("Chat");
|
||||
c.setType(ConversationType.INDIVIDUAL);
|
||||
assertThat(c.getUpdatedAt()).isNull();
|
||||
c.onUpdate();
|
||||
assertThat(c.getUpdatedAt()).isNotNull();
|
||||
c.setStatut(StatutConversation.ACTIVE);
|
||||
c.archiver();
|
||||
assertThat(c.getStatut()).isEqualTo(StatutConversation.ARCHIVEE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("enregistrerNouveauMessage() — incrémente compteur et met à jour horodatage")
|
||||
void enregistrerNouveauMessage() {
|
||||
Conversation c = new Conversation();
|
||||
c.setNombreMessages(2);
|
||||
LocalDateTime avant = LocalDateTime.now().minusSeconds(1);
|
||||
|
||||
c.enregistrerNouveauMessage();
|
||||
|
||||
assertThat(c.getNombreMessages()).isEqualTo(3);
|
||||
assertThat(c.getDernierMessageAt()).isNotNull().isAfter(avant);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("enregistrerNouveauMessage() — nombreMessages null → passe à 1")
|
||||
void enregistrerNouveauMessage_nullCount() {
|
||||
Conversation c = new Conversation();
|
||||
c.setNombreMessages(null);
|
||||
c.enregistrerNouveauMessage();
|
||||
assertThat(c.getNombreMessages()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate() — statut null → initialisé à ACTIVE")
|
||||
void onCreate_statut() {
|
||||
Conversation c = new Conversation();
|
||||
c.setStatut(null);
|
||||
c.setTypeConversation(TypeConversation.ROLE_CANAL);
|
||||
c.setId(java.util.UUID.randomUUID());
|
||||
c.setDateCreation(LocalDateTime.now());
|
||||
c.setActif(true);
|
||||
c.setVersion(0L);
|
||||
c.onCreate();
|
||||
assertThat(c.getStatut()).isEqualTo(StatutConversation.ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("MemberBlock")
|
||||
class MemberBlockTest {
|
||||
|
||||
private static Membre newMembre(String email) {
|
||||
Membre m = new Membre();
|
||||
m.setId(UUID.randomUUID());
|
||||
m.setNumeroMembre("M-" + UUID.randomUUID().toString().substring(0, 4));
|
||||
m.setPrenom("Test");
|
||||
m.setNom("User");
|
||||
m.setEmail(email);
|
||||
m.setDateNaissance(LocalDate.now());
|
||||
return m;
|
||||
}
|
||||
|
||||
private static Organisation newOrg() {
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Org Test");
|
||||
return org;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getters/setters")
|
||||
void gettersSetters() {
|
||||
Membre bloqueur = newMembre("bloqueur@test.com");
|
||||
Membre bloque = newMembre("bloque@test.com");
|
||||
Organisation org = newOrg();
|
||||
|
||||
MemberBlock block = new MemberBlock();
|
||||
block.setBloqueur(bloqueur);
|
||||
block.setBloque(bloque);
|
||||
block.setOrganisation(org);
|
||||
|
||||
assertThat(block.getBloqueur()).isEqualTo(bloqueur);
|
||||
assertThat(block.getBloque()).isEqualTo(bloque);
|
||||
assertThat(block.getOrganisation()).isEqualTo(org);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("builder — tous les champs")
|
||||
void builder_allFields() {
|
||||
Membre bloqueur = newMembre("a@test.com");
|
||||
Membre bloque = newMembre("b@test.com");
|
||||
Organisation org = newOrg();
|
||||
|
||||
MemberBlock block = MemberBlock.builder()
|
||||
.bloqueur(bloqueur)
|
||||
.bloque(bloque)
|
||||
.organisation(org)
|
||||
.build();
|
||||
|
||||
assertThat(block.getBloqueur()).isEqualTo(bloqueur);
|
||||
assertThat(block.getBloque()).isEqualTo(bloque);
|
||||
assertThat(block.getOrganisation()).isEqualTo(org);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("equals et hashCode")
|
||||
void equalsHashCode() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Membre bloqueur = newMembre("a@test.com");
|
||||
Membre bloque = newMembre("b@test.com");
|
||||
Organisation org = newOrg();
|
||||
|
||||
MemberBlock a = MemberBlock.builder().bloqueur(bloqueur).bloque(bloque).organisation(org).build();
|
||||
a.setId(id);
|
||||
MemberBlock b = MemberBlock.builder().bloqueur(bloqueur).bloque(bloque).organisation(org).build();
|
||||
b.setId(id);
|
||||
|
||||
assertThat(a).isEqualTo(b);
|
||||
assertThat(a.hashCode()).isEqualTo(b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toString non null")
|
||||
void toString_nonNull() {
|
||||
MemberBlock block = MemberBlock.builder()
|
||||
.bloqueur(newMembre("a@test.com"))
|
||||
.bloque(newMembre("b@test.com"))
|
||||
.organisation(newOrg())
|
||||
.build();
|
||||
assertThat(block.toString()).isNotNull().isNotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,173 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("Message")
|
||||
class MessageTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("markAsRead sets status to READ and sets readAt")
|
||||
void markAsRead_setsStatusAndReadAt() {
|
||||
Message m = new Message();
|
||||
m.setStatus(MessageStatus.SENT);
|
||||
assertThat(m.getReadAt()).isNull();
|
||||
private static Membre newMembre() {
|
||||
Membre m = new Membre();
|
||||
m.setId(UUID.randomUUID());
|
||||
m.setNumeroMembre("M1");
|
||||
m.setPrenom("Alpha");
|
||||
m.setNom("Diallo");
|
||||
m.setEmail("alpha@test.com");
|
||||
m.setDateNaissance(LocalDate.now());
|
||||
return m;
|
||||
}
|
||||
|
||||
m.markAsRead();
|
||||
|
||||
assertThat(m.getStatus()).isEqualTo(MessageStatus.READ);
|
||||
assertThat(m.getReadAt()).isNotNull();
|
||||
private static Conversation newConversation() {
|
||||
Organisation org = new Organisation();
|
||||
org.setId(UUID.randomUUID());
|
||||
org.setNom("Org Test");
|
||||
Conversation c = new Conversation();
|
||||
c.setId(UUID.randomUUID());
|
||||
c.setOrganisation(org);
|
||||
c.setTypeConversation(TypeConversation.DIRECTE);
|
||||
c.setStatut(StatutConversation.ACTIVE);
|
||||
c.setNombreMessages(0);
|
||||
return c;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("markAsEdited sets isEdited true and sets editedAt")
|
||||
void markAsEdited_setsIsEditedAndEditedAt() {
|
||||
Message m = new Message();
|
||||
assertThat(m.getIsEdited()).isFalse();
|
||||
assertThat(m.getEditedAt()).isNull();
|
||||
@DisplayName("getters/setters — message texte")
|
||||
void gettersSetters_texte() {
|
||||
Message msg = new Message();
|
||||
Conversation conv = newConversation();
|
||||
Membre expediteur = newMembre();
|
||||
msg.setConversation(conv);
|
||||
msg.setExpediteur(expediteur);
|
||||
msg.setTypeMessage(TypeContenu.TEXTE);
|
||||
msg.setContenu("Bonjour tout le monde !");
|
||||
|
||||
m.markAsEdited();
|
||||
assertThat(msg.getConversation()).isEqualTo(conv);
|
||||
assertThat(msg.getExpediteur()).isEqualTo(expediteur);
|
||||
assertThat(msg.getTypeMessage()).isEqualTo(TypeContenu.TEXTE);
|
||||
assertThat(msg.getContenu()).isEqualTo("Bonjour tout le monde !");
|
||||
}
|
||||
|
||||
assertThat(m.getIsEdited()).isTrue();
|
||||
assertThat(m.getEditedAt()).isNotNull();
|
||||
@Test
|
||||
@DisplayName("getters/setters — note vocale")
|
||||
void gettersSetters_vocal() {
|
||||
Message msg = new Message();
|
||||
msg.setConversation(newConversation());
|
||||
msg.setExpediteur(newMembre());
|
||||
msg.setTypeMessage(TypeContenu.VOCAL);
|
||||
msg.setUrlFichier("https://storage.example.com/audio.opus");
|
||||
msg.setDureeAudio(45);
|
||||
|
||||
assertThat(msg.getTypeMessage()).isEqualTo(TypeContenu.VOCAL);
|
||||
assertThat(msg.getUrlFichier()).isEqualTo("https://storage.example.com/audio.opus");
|
||||
assertThat(msg.getDureeAudio()).isEqualTo(45);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("estTextuel — TEXTE → true")
|
||||
void estTextuel_true() {
|
||||
Message msg = buildMinimal(TypeContenu.TEXTE);
|
||||
assertThat(msg.estTextuel()).isTrue();
|
||||
assertThat(msg.estVocal()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("estVocal — VOCAL → true")
|
||||
void estVocal_true() {
|
||||
Message msg = buildMinimal(TypeContenu.VOCAL);
|
||||
assertThat(msg.estVocal()).isTrue();
|
||||
assertThat(msg.estTextuel()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("estSupprime — null → false")
|
||||
void estSupprime_nullFalse() {
|
||||
Message msg = buildMinimal(TypeContenu.TEXTE);
|
||||
msg.setSupprimeLe(null);
|
||||
assertThat(msg.estSupprime()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("supprimer — contenu remplacé par marqueur, urlFichier null")
|
||||
void supprimer() {
|
||||
Message msg = buildMinimal(TypeContenu.TEXTE);
|
||||
msg.setContenu("Message secret");
|
||||
msg.setUrlFichier("https://example.com/file");
|
||||
|
||||
msg.supprimer();
|
||||
|
||||
assertThat(msg.estSupprime()).isTrue();
|
||||
assertThat(msg.getContenu()).isEqualTo("[Message supprimé]");
|
||||
assertThat(msg.getUrlFichier()).isNull();
|
||||
assertThat(msg.getSupprimeLe()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate — initialise typeMessage si null")
|
||||
void onCreate_initDefaults() {
|
||||
Message msg = new Message();
|
||||
msg.setConversation(newConversation());
|
||||
msg.setExpediteur(newMembre());
|
||||
msg.setContenu("Test");
|
||||
|
||||
msg.onCreate();
|
||||
|
||||
assertThat(msg.getTypeMessage()).isEqualTo(TypeContenu.TEXTE);
|
||||
assertThat(msg.getDateCreation()).isNotNull();
|
||||
assertThat(msg.getActif()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate — préserve typeMessage existant")
|
||||
void onCreate_preservesType() {
|
||||
Message msg = new Message();
|
||||
msg.setConversation(newConversation());
|
||||
msg.setExpediteur(newMembre());
|
||||
msg.setTypeMessage(TypeContenu.VOCAL);
|
||||
msg.setUrlFichier("url");
|
||||
msg.setDureeAudio(30);
|
||||
|
||||
msg.onCreate();
|
||||
|
||||
assertThat(msg.getTypeMessage()).isEqualTo(TypeContenu.VOCAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("equals et hashCode")
|
||||
void equalsHashCode() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Message a = buildMinimal(TypeContenu.TEXTE);
|
||||
a.setId(id);
|
||||
Message b = buildMinimal(TypeContenu.TEXTE);
|
||||
b.setId(id);
|
||||
assertThat(a).isEqualTo(b);
|
||||
assertThat(a.hashCode()).isEqualTo(b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toString non null")
|
||||
void toString_nonNull() {
|
||||
assertThat(buildMinimal(TypeContenu.TEXTE).toString()).isNotNull().isNotEmpty();
|
||||
}
|
||||
|
||||
private Message buildMinimal(TypeContenu type) {
|
||||
Message msg = new Message();
|
||||
msg.setConversation(newConversation());
|
||||
msg.setExpediteur(newMembre());
|
||||
msg.setTypeMessage(type);
|
||||
if (TypeContenu.TEXTE.equals(type)) {
|
||||
msg.setContenu("Texte test");
|
||||
} else if (TypeContenu.VOCAL.equals(type)) {
|
||||
msg.setUrlFichier("https://example.com/audio.opus");
|
||||
msg.setDureeAudio(30);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("PaiementObjet")
|
||||
class PaiementObjetTest {
|
||||
|
||||
private static Paiement newPaiement() {
|
||||
Membre m = new Membre();
|
||||
m.setId(UUID.randomUUID());
|
||||
m.setNumeroMembre("M1");
|
||||
m.setPrenom("A");
|
||||
m.setNom("B");
|
||||
m.setEmail("a@test.com");
|
||||
m.setDateNaissance(java.time.LocalDate.now());
|
||||
Paiement p = new Paiement();
|
||||
p.setId(UUID.randomUUID());
|
||||
p.setNumeroReference("PAY-1");
|
||||
p.setMontant(BigDecimal.TEN);
|
||||
p.setCodeDevise("XOF");
|
||||
p.setMethodePaiement("WAVE");
|
||||
p.setMembre(m);
|
||||
return p;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getters/setters")
|
||||
void gettersSetters() {
|
||||
PaiementObjet po = new PaiementObjet();
|
||||
po.setPaiement(newPaiement());
|
||||
po.setTypeObjetCible("COTISATION");
|
||||
po.setObjetCibleId(UUID.randomUUID());
|
||||
po.setMontantApplique(new BigDecimal("5000.00"));
|
||||
po.setDateApplication(LocalDateTime.now());
|
||||
po.setCommentaire("Cotisation janvier");
|
||||
|
||||
assertThat(po.getTypeObjetCible()).isEqualTo("COTISATION");
|
||||
assertThat(po.getMontantApplique()).isEqualByComparingTo("5000.00");
|
||||
assertThat(po.getCommentaire()).isEqualTo("Cotisation janvier");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("equals et hashCode")
|
||||
void equalsHashCode() {
|
||||
UUID id = UUID.randomUUID();
|
||||
UUID objId = UUID.randomUUID();
|
||||
Paiement p = newPaiement();
|
||||
PaiementObjet a = new PaiementObjet();
|
||||
a.setId(id);
|
||||
a.setPaiement(p);
|
||||
a.setTypeObjetCible("COTISATION");
|
||||
a.setObjetCibleId(objId);
|
||||
a.setMontantApplique(BigDecimal.ONE);
|
||||
PaiementObjet b = new PaiementObjet();
|
||||
b.setId(id);
|
||||
b.setPaiement(p);
|
||||
b.setTypeObjetCible("COTISATION");
|
||||
b.setObjetCibleId(objId);
|
||||
b.setMontantApplique(BigDecimal.ONE);
|
||||
assertThat(a).isEqualTo(b);
|
||||
assertThat(a.hashCode()).isEqualTo(b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toString non null")
|
||||
void toString_nonNull() {
|
||||
PaiementObjet po = new PaiementObjet();
|
||||
po.setPaiement(newPaiement());
|
||||
po.setTypeObjetCible("COTISATION");
|
||||
po.setObjetCibleId(UUID.randomUUID());
|
||||
po.setMontantApplique(BigDecimal.ONE);
|
||||
assertThat(po.toString()).isNotNull().isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate initialise dateApplication si null")
|
||||
void onCreate_setsDateApplicationWhenNull() {
|
||||
PaiementObjet po = new PaiementObjet();
|
||||
po.setPaiement(newPaiement());
|
||||
po.setTypeObjetCible("COTISATION");
|
||||
po.setObjetCibleId(UUID.randomUUID());
|
||||
po.setMontantApplique(BigDecimal.ONE);
|
||||
// dateApplication est null
|
||||
|
||||
po.onCreate();
|
||||
|
||||
assertThat(po.getDateApplication()).isNotNull();
|
||||
assertThat(po.getActif()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate ne remplace pas une dateApplication existante")
|
||||
void onCreate_doesNotOverrideDateApplication() {
|
||||
LocalDateTime fixed = LocalDateTime.of(2026, 1, 1, 0, 0);
|
||||
PaiementObjet po = new PaiementObjet();
|
||||
po.setDateApplication(fixed);
|
||||
|
||||
po.onCreate();
|
||||
|
||||
assertThat(po.getDateApplication()).isEqualTo(fixed);
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("Paiement")
|
||||
class PaiementTest {
|
||||
|
||||
private static Membre newMembre() {
|
||||
Membre m = new Membre();
|
||||
m.setId(UUID.randomUUID());
|
||||
m.setNumeroMembre("M1");
|
||||
m.setPrenom("A");
|
||||
m.setNom("B");
|
||||
m.setEmail("a@test.com");
|
||||
m.setDateNaissance(java.time.LocalDate.now());
|
||||
return m;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getters/setters")
|
||||
void gettersSetters() {
|
||||
Paiement p = new Paiement();
|
||||
p.setNumeroReference("PAY-2025-001");
|
||||
p.setMontant(new BigDecimal("10000.00"));
|
||||
p.setCodeDevise("XOF");
|
||||
p.setMethodePaiement("WAVE");
|
||||
p.setStatutPaiement("VALIDE");
|
||||
p.setDatePaiement(LocalDateTime.now());
|
||||
p.setMembre(newMembre());
|
||||
|
||||
assertThat(p.getNumeroReference()).isEqualTo("PAY-2025-001");
|
||||
assertThat(p.getMontant()).isEqualByComparingTo("10000.00");
|
||||
assertThat(p.getCodeDevise()).isEqualTo("XOF");
|
||||
assertThat(p.getStatutPaiement()).isEqualTo("VALIDE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("genererNumeroReference")
|
||||
void genererNumeroReference() {
|
||||
String ref = Paiement.genererNumeroReference();
|
||||
assertThat(ref).startsWith("PAY-").isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isValide et peutEtreModifie")
|
||||
void isValide_peutEtreModifie() {
|
||||
Paiement p = new Paiement();
|
||||
p.setNumeroReference("X");
|
||||
p.setMontant(BigDecimal.ONE);
|
||||
p.setCodeDevise("XOF");
|
||||
p.setMethodePaiement("WAVE");
|
||||
p.setMembre(newMembre());
|
||||
p.setStatutPaiement("VALIDE");
|
||||
assertThat(p.isValide()).isTrue();
|
||||
assertThat(p.peutEtreModifie()).isFalse();
|
||||
p.setStatutPaiement("EN_ATTENTE");
|
||||
assertThat(p.peutEtreModifie()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("equals et hashCode")
|
||||
void equalsHashCode() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Membre m = newMembre();
|
||||
Paiement a = new Paiement();
|
||||
a.setId(id);
|
||||
a.setNumeroReference("REF-1");
|
||||
a.setMontant(BigDecimal.ONE);
|
||||
a.setCodeDevise("XOF");
|
||||
a.setMethodePaiement("WAVE");
|
||||
a.setMembre(m);
|
||||
Paiement b = new Paiement();
|
||||
b.setId(id);
|
||||
b.setNumeroReference("REF-1");
|
||||
b.setMontant(BigDecimal.ONE);
|
||||
b.setCodeDevise("XOF");
|
||||
b.setMethodePaiement("WAVE");
|
||||
b.setMembre(m);
|
||||
assertThat(a).isEqualTo(b);
|
||||
assertThat(a.hashCode()).isEqualTo(b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toString non null")
|
||||
void toString_nonNull() {
|
||||
Paiement p = new Paiement();
|
||||
p.setNumeroReference("X");
|
||||
p.setMontant(BigDecimal.ONE);
|
||||
p.setCodeDevise("XOF");
|
||||
p.setMethodePaiement("WAVE");
|
||||
p.setMembre(newMembre());
|
||||
assertThat(p.toString()).isNotNull().isNotEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("VersementObjet")
|
||||
class VersementObjetTest {
|
||||
|
||||
private static Versement newVersement() {
|
||||
Membre m = new Membre();
|
||||
m.setId(UUID.randomUUID());
|
||||
m.setNumeroMembre("M1");
|
||||
m.setPrenom("A");
|
||||
m.setNom("B");
|
||||
m.setEmail("a@test.com");
|
||||
m.setDateNaissance(java.time.LocalDate.now());
|
||||
Versement v = new Versement();
|
||||
v.setId(UUID.randomUUID());
|
||||
v.setNumeroReference("VRS-2026-001");
|
||||
v.setMontant(BigDecimal.TEN);
|
||||
v.setCodeDevise("XOF");
|
||||
v.setMethodePaiement("WAVE");
|
||||
v.setMembre(m);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getters/setters")
|
||||
void gettersSetters() {
|
||||
VersementObjet vo = new VersementObjet();
|
||||
vo.setVersement(newVersement());
|
||||
vo.setTypeObjetCible("COTISATION");
|
||||
vo.setObjetCibleId(UUID.randomUUID());
|
||||
vo.setMontantApplique(new BigDecimal("5000.00"));
|
||||
vo.setDateApplication(LocalDateTime.now());
|
||||
vo.setCommentaire("Cotisation janvier");
|
||||
|
||||
assertThat(vo.getTypeObjetCible()).isEqualTo("COTISATION");
|
||||
assertThat(vo.getMontantApplique()).isEqualByComparingTo("5000.00");
|
||||
assertThat(vo.getCommentaire()).isEqualTo("Cotisation janvier");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("equals et hashCode")
|
||||
void equalsHashCode() {
|
||||
UUID id = UUID.randomUUID();
|
||||
UUID objId = UUID.randomUUID();
|
||||
Versement v = newVersement();
|
||||
VersementObjet a = new VersementObjet();
|
||||
a.setId(id);
|
||||
a.setVersement(v);
|
||||
a.setTypeObjetCible("COTISATION");
|
||||
a.setObjetCibleId(objId);
|
||||
a.setMontantApplique(BigDecimal.ONE);
|
||||
VersementObjet b = new VersementObjet();
|
||||
b.setId(id);
|
||||
b.setVersement(v);
|
||||
b.setTypeObjetCible("COTISATION");
|
||||
b.setObjetCibleId(objId);
|
||||
b.setMontantApplique(BigDecimal.ONE);
|
||||
assertThat(a).isEqualTo(b);
|
||||
assertThat(a.hashCode()).isEqualTo(b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toString non null")
|
||||
void toString_nonNull() {
|
||||
VersementObjet vo = new VersementObjet();
|
||||
vo.setVersement(newVersement());
|
||||
vo.setTypeObjetCible("COTISATION");
|
||||
vo.setObjetCibleId(UUID.randomUUID());
|
||||
vo.setMontantApplique(BigDecimal.ONE);
|
||||
assertThat(vo.toString()).isNotNull().isNotEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate initialise dateApplication si null")
|
||||
void onCreate_setsDateApplicationWhenNull() {
|
||||
VersementObjet vo = new VersementObjet();
|
||||
vo.setVersement(newVersement());
|
||||
vo.setTypeObjetCible("COTISATION");
|
||||
vo.setObjetCibleId(UUID.randomUUID());
|
||||
vo.setMontantApplique(BigDecimal.ONE);
|
||||
|
||||
vo.onCreate();
|
||||
|
||||
assertThat(vo.getDateApplication()).isNotNull();
|
||||
assertThat(vo.getActif()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate ne remplace pas une dateApplication existante")
|
||||
void onCreate_doesNotOverrideDateApplication() {
|
||||
LocalDateTime fixed = LocalDateTime.of(2026, 1, 1, 0, 0);
|
||||
VersementObjet vo = new VersementObjet();
|
||||
vo.setDateApplication(fixed);
|
||||
|
||||
vo.onCreate();
|
||||
|
||||
assertThat(vo.getDateApplication()).isEqualTo(fixed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package dev.lions.unionflow.server.entity;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("Versement")
|
||||
class VersementTest {
|
||||
|
||||
private static Membre newMembre() {
|
||||
Membre m = new Membre();
|
||||
m.setId(UUID.randomUUID());
|
||||
m.setNumeroMembre("M1");
|
||||
m.setPrenom("A");
|
||||
m.setNom("B");
|
||||
m.setEmail("a@test.com");
|
||||
m.setDateNaissance(java.time.LocalDate.now());
|
||||
return m;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getters/setters")
|
||||
void gettersSetters() {
|
||||
Versement v = new Versement();
|
||||
v.setNumeroReference("VRS-2026-001");
|
||||
v.setMontant(new BigDecimal("10000.00"));
|
||||
v.setCodeDevise("XOF");
|
||||
v.setMethodePaiement("WAVE");
|
||||
v.setStatutPaiement("CONFIRME");
|
||||
v.setDatePaiement(LocalDateTime.now());
|
||||
v.setNumeroTelephone("771234567");
|
||||
v.setMembre(newMembre());
|
||||
|
||||
assertThat(v.getNumeroReference()).isEqualTo("VRS-2026-001");
|
||||
assertThat(v.getMontant()).isEqualByComparingTo("10000.00");
|
||||
assertThat(v.getCodeDevise()).isEqualTo("XOF");
|
||||
assertThat(v.getStatutPaiement()).isEqualTo("CONFIRME");
|
||||
assertThat(v.getNumeroTelephone()).isEqualTo("771234567");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("genererNumeroReference commence par VRS-")
|
||||
void genererNumeroReference_startsWithVRS() {
|
||||
String ref = Versement.genererNumeroReference();
|
||||
assertThat(ref).startsWith("VRS-").isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("genererNumeroReference retourne des références uniques")
|
||||
void genererNumeroReference_unique() {
|
||||
String ref1 = Versement.genererNumeroReference();
|
||||
String ref2 = Versement.genererNumeroReference();
|
||||
assertThat(ref1).isNotEqualTo(ref2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isConfirme — statut CONFIRME")
|
||||
void isConfirme_statutCONFIRME() {
|
||||
Versement v = buildMinimal();
|
||||
v.setStatutPaiement("CONFIRME");
|
||||
assertThat(v.isConfirme()).isTrue();
|
||||
assertThat(v.peutEtreModifie()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isConfirme — statut VALIDE (compatibilité)")
|
||||
void isConfirme_statutVALIDE() {
|
||||
Versement v = buildMinimal();
|
||||
v.setStatutPaiement("VALIDE");
|
||||
assertThat(v.isConfirme()).isTrue();
|
||||
assertThat(v.peutEtreModifie()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isConfirme — statut EN_ATTENTE")
|
||||
void isConfirme_statutEnAttente() {
|
||||
Versement v = buildMinimal();
|
||||
v.setStatutPaiement("EN_ATTENTE");
|
||||
assertThat(v.isConfirme()).isFalse();
|
||||
assertThat(v.peutEtreModifie()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("peutEtreModifie — statut ANNULE")
|
||||
void peutEtreModifie_false_whenANNULE() {
|
||||
Versement v = buildMinimal();
|
||||
v.setStatutPaiement("ANNULE");
|
||||
assertThat(v.peutEtreModifie()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("peutEtreModifie — statut EN_ATTENTE_VALIDATION")
|
||||
void peutEtreModifie_true_whenEnAttenteValidation() {
|
||||
Versement v = buildMinimal();
|
||||
v.setStatutPaiement("EN_ATTENTE_VALIDATION");
|
||||
assertThat(v.peutEtreModifie()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate initialise reference, statut et datePaiement si null")
|
||||
void onCreate_initDefaults() {
|
||||
Versement v = new Versement();
|
||||
v.setMontant(BigDecimal.TEN);
|
||||
v.setCodeDevise("XOF");
|
||||
v.setMethodePaiement("WAVE");
|
||||
v.setMembre(newMembre());
|
||||
|
||||
v.onCreate();
|
||||
|
||||
assertThat(v.getNumeroReference()).startsWith("VRS-");
|
||||
assertThat(v.getStatutPaiement()).isEqualTo("EN_ATTENTE");
|
||||
assertThat(v.getDatePaiement()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("onCreate conserve la référence existante")
|
||||
void onCreate_preservesExistingReference() {
|
||||
Versement v = buildMinimal();
|
||||
v.setNumeroReference("VRS-CUSTOM-001");
|
||||
v.onCreate();
|
||||
assertThat(v.getNumeroReference()).isEqualTo("VRS-CUSTOM-001");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("equals et hashCode")
|
||||
void equalsHashCode() {
|
||||
UUID id = UUID.randomUUID();
|
||||
Membre m = newMembre();
|
||||
Versement a = buildMinimal();
|
||||
a.setId(id);
|
||||
a.setMembre(m);
|
||||
Versement b = buildMinimal();
|
||||
b.setId(id);
|
||||
b.setMembre(m);
|
||||
assertThat(a).isEqualTo(b);
|
||||
assertThat(a.hashCode()).isEqualTo(b.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toString non null")
|
||||
void toString_nonNull() {
|
||||
assertThat(buildMinimal().toString()).isNotNull().isNotEmpty();
|
||||
}
|
||||
|
||||
private Versement buildMinimal() {
|
||||
Versement v = new Versement();
|
||||
v.setNumeroReference("VRS-2026-TEST");
|
||||
v.setMontant(BigDecimal.ONE);
|
||||
v.setCodeDevise("XOF");
|
||||
v.setMethodePaiement("WAVE");
|
||||
v.setStatutPaiement("EN_ATTENTE");
|
||||
v.setMembre(newMembre());
|
||||
return v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.ContactPolicy;
|
||||
import io.quarkus.test.TestTransaction;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@QuarkusTest
|
||||
@DisplayName("ContactPolicyRepository")
|
||||
class ContactPolicyRepositoryTest {
|
||||
|
||||
@Inject
|
||||
ContactPolicyRepository contactPolicyRepository;
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByOrganisationId retourne empty pour organisation inexistante")
|
||||
void findByOrganisationId_inexistant_returnsEmpty() {
|
||||
Optional<ContactPolicy> opt = contactPolicyRepository.findByOrganisationId(UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("listAll retourne liste non nulle")
|
||||
void listAll_returnsNonNull() {
|
||||
assertThat(contactPolicyRepository.listAll()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("count retourne >= 0")
|
||||
void count_returnsNonNegative() {
|
||||
assertThat(contactPolicyRepository.count()).isGreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.ConversationParticipant;
|
||||
import io.quarkus.test.TestTransaction;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@QuarkusTest
|
||||
@DisplayName("ConversationParticipantRepository")
|
||||
class ConversationParticipantRepositoryTest {
|
||||
|
||||
@Inject
|
||||
ConversationParticipantRepository conversationParticipantRepository;
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findParticipant retourne empty pour conversation inexistante")
|
||||
void findParticipant_returnsEmpty() {
|
||||
Optional<ConversationParticipant> opt =
|
||||
conversationParticipantRepository.findParticipant(UUID.randomUUID(), UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByConversation retourne liste vide pour conversation inexistante")
|
||||
void findByConversation_returnsEmpty() {
|
||||
List<ConversationParticipant> list =
|
||||
conversationParticipantRepository.findByConversation(UUID.randomUUID());
|
||||
assertThat(list).isNotNull().isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("estParticipant retourne false pour conversation inexistante")
|
||||
void estParticipant_returnsFalse() {
|
||||
boolean result = conversationParticipantRepository.estParticipant(
|
||||
UUID.randomUUID(), UUID.randomUUID());
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("count retourne >= 0")
|
||||
void count_returnsNonNegative() {
|
||||
assertThat(conversationParticipantRepository.count()).isGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("listAll retourne liste non nulle")
|
||||
void listAll_returnsNonNull() {
|
||||
assertThat(conversationParticipantRepository.listAll()).isNotNull();
|
||||
}
|
||||
}
|
||||
@@ -1,149 +1,85 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.communication.ConversationType;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
|
||||
import dev.lions.unionflow.server.entity.Conversation;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import io.quarkus.test.TestTransaction;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests d'intégration pour {@link ConversationRepository}.
|
||||
* Couvre les 3 méthodes : findByParticipant, findByIdAndParticipant, findByOrganisation.
|
||||
*/
|
||||
@QuarkusTest
|
||||
@DisplayName("ConversationRepository")
|
||||
class ConversationRepositoryTest {
|
||||
|
||||
@Inject
|
||||
ConversationRepository conversationRepository;
|
||||
|
||||
@Inject
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
private Organisation createOrganisation() {
|
||||
Organisation o = new Organisation();
|
||||
o.setNom("Org Conversation");
|
||||
o.setTypeOrganisation("ASSOCIATION");
|
||||
o.setStatut("ACTIVE");
|
||||
o.setEmail("conv-" + UUID.randomUUID() + "@test.com");
|
||||
o.setActif(true);
|
||||
o.setDateCreation(LocalDateTime.now());
|
||||
organisationRepository.persist(o);
|
||||
return o;
|
||||
}
|
||||
|
||||
private Conversation createConversation(String name, Organisation org) {
|
||||
Conversation c = new Conversation();
|
||||
c.setName(name);
|
||||
c.setType(ConversationType.GROUP);
|
||||
c.setOrganisation(org);
|
||||
c.setActif(true);
|
||||
c.setDateCreation(LocalDateTime.now());
|
||||
conversationRepository.persist(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// findByParticipant — branches avec includeArchived=true et false
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByParticipant avec includeArchived=true retourne liste vide si aucune conversation")
|
||||
void findByParticipant_noConversations_returnsEmptyList() {
|
||||
UUID randomMembre = UUID.randomUUID();
|
||||
|
||||
List<Conversation> result = conversationRepository.findByParticipant(randomMembre, true);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).isEmpty();
|
||||
@DisplayName("findById retourne null pour UUID inexistant")
|
||||
void findById_inexistant_returnsNull() {
|
||||
assertThat(conversationRepository.findById(UUID.randomUUID())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByParticipant avec includeArchived=false retourne liste vide si aucune conversation")
|
||||
void findByParticipant_excludeArchived_returnsEmptyList() {
|
||||
UUID randomMembre = UUID.randomUUID();
|
||||
|
||||
List<Conversation> result = conversationRepository.findByParticipant(randomMembre, false);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// findByIdAndParticipant
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByIdAndParticipant retourne empty pour ID et membreId inconnus")
|
||||
void findByIdAndParticipant_unknownIds_returnsEmpty() {
|
||||
Optional<Conversation> result = conversationRepository.findByIdAndParticipant(
|
||||
UUID.randomUUID(), UUID.randomUUID());
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
@DisplayName("findConversationById retourne empty pour UUID inexistant")
|
||||
void findConversationById_inexistant_returnsEmpty() {
|
||||
Optional<Conversation> opt = conversationRepository.findConversationById(UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByIdAndParticipant retourne empty pour conversationId inexistant")
|
||||
void findByIdAndParticipant_nonExistentConversation_returnsEmpty() {
|
||||
UUID nonExistentId = UUID.randomUUID();
|
||||
UUID membreId = UUID.randomUUID();
|
||||
|
||||
Optional<Conversation> result = conversationRepository.findByIdAndParticipant(
|
||||
nonExistentId, membreId);
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// findByOrganisation
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByOrganisation retourne liste vide si organisation sans conversation")
|
||||
void findByOrganisation_noConversations_returnsEmptyList() {
|
||||
Organisation org = createOrganisation();
|
||||
|
||||
List<Conversation> result = conversationRepository.findByOrganisation(org.getId());
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).isEmpty();
|
||||
@DisplayName("findByMembreId retourne liste non nulle pour membre inexistant")
|
||||
void findByMembreId_inconnu_returnsEmpty() {
|
||||
List<Conversation> list = conversationRepository.findByMembreId(UUID.randomUUID());
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByOrganisation retourne les conversations de l'organisation persistées")
|
||||
void findByOrganisation_withConversations_returnsList() {
|
||||
Organisation org = createOrganisation();
|
||||
createConversation("Conv1-" + UUID.randomUUID(), org);
|
||||
createConversation("Conv2-" + UUID.randomUUID(), org);
|
||||
|
||||
List<Conversation> result = conversationRepository.findByOrganisation(org.getId());
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).hasSize(2);
|
||||
@DisplayName("findConversationDirecte retourne empty si aucune conversation")
|
||||
void findConversationDirecte_inconnu_returnsEmpty() {
|
||||
Optional<Conversation> opt = conversationRepository.findConversationDirecte(
|
||||
UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByOrganisation retourne liste vide pour organisationId inexistant")
|
||||
void findByOrganisation_unknownOrganisationId_returnsEmptyList() {
|
||||
List<Conversation> result = conversationRepository.findByOrganisation(UUID.randomUUID());
|
||||
@DisplayName("findCanalRole retourne empty si aucun canal")
|
||||
void findCanalRole_inconnu_returnsEmpty() {
|
||||
Optional<Conversation> opt = conversationRepository.findCanalRole(UUID.randomUUID(), "TRESORIER");
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).isEmpty();
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findActivesByMembre retourne liste non nulle")
|
||||
void findActivesByMembre_returnsNonNull() {
|
||||
List<Conversation> list = conversationRepository.findActivesByMembre(UUID.randomUUID());
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("count retourne un nombre >= 0")
|
||||
void count_returnsNonNegative() {
|
||||
assertThat(conversationRepository.count()).isGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("listAll retourne une liste non nulle")
|
||||
void listAll_returnsNonNull() {
|
||||
assertThat(conversationRepository.listAll()).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.entity.MemberBlock;
|
||||
import io.quarkus.test.TestTransaction;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@QuarkusTest
|
||||
@DisplayName("MemberBlockRepository")
|
||||
class MemberBlockRepositoryTest {
|
||||
|
||||
@Inject
|
||||
MemberBlockRepository memberBlockRepository;
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("estBloque retourne false si aucun blocage")
|
||||
void estBloque_returns_false() {
|
||||
boolean result = memberBlockRepository.estBloque(
|
||||
UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID());
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findBlocage retourne empty si aucun blocage")
|
||||
void findBlocage_returnsEmpty() {
|
||||
Optional<MemberBlock> opt = memberBlockRepository.findBlocage(
|
||||
UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByBloqueur retourne liste vide pour membre inexistant")
|
||||
void findByBloqueur_returnsEmpty() {
|
||||
List<MemberBlock> list = memberBlockRepository.findByBloqueur(UUID.randomUUID());
|
||||
assertThat(list).isNotNull().isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByBloqueurEtOrganisation retourne liste vide")
|
||||
void findByBloqueurEtOrganisation_returnsEmpty() {
|
||||
List<MemberBlock> list = memberBlockRepository.findByBloqueurEtOrganisation(
|
||||
UUID.randomUUID(), UUID.randomUUID());
|
||||
assertThat(list).isNotNull().isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("count retourne >= 0")
|
||||
void count_returnsNonNegative() {
|
||||
assertThat(memberBlockRepository.count()).isGreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
@@ -1,256 +1,69 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.communication.ConversationType;
|
||||
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 dev.lions.unionflow.server.entity.Conversation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Message;
|
||||
import io.quarkus.test.TestTransaction;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@QuarkusTest
|
||||
@DisplayName("MessageRepository — tests des méthodes de requête")
|
||||
@DisplayName("MessageRepository")
|
||||
class MessageRepositoryTest {
|
||||
|
||||
@Inject
|
||||
MessageRepository messageRepository;
|
||||
|
||||
@Inject
|
||||
EntityManager em;
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Membre persistMembre() {
|
||||
Membre m = new Membre();
|
||||
m.setNumeroMembre("MSG-MEM-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
|
||||
m.setPrenom("Jean");
|
||||
m.setNom("Messagerie");
|
||||
m.setEmail("msg." + UUID.randomUUID() + "@test.com");
|
||||
m.setDateNaissance(LocalDate.of(1990, 1, 1));
|
||||
m.setActif(true);
|
||||
em.persist(m);
|
||||
em.flush();
|
||||
return m;
|
||||
}
|
||||
|
||||
private Conversation persistConversation() {
|
||||
Conversation conv = new Conversation();
|
||||
conv.setName("Conv Test " + UUID.randomUUID().toString().substring(0, 8));
|
||||
conv.setType(ConversationType.GROUP);
|
||||
conv.setActif(true);
|
||||
em.persist(conv);
|
||||
em.flush();
|
||||
return conv;
|
||||
}
|
||||
|
||||
private Message persistMessage(Conversation conv, Membre sender, MessageStatus status) {
|
||||
Message msg = new Message();
|
||||
msg.setConversation(conv);
|
||||
msg.setSender(sender);
|
||||
msg.setSenderName(sender.getPrenom() + " " + sender.getNom());
|
||||
msg.setContent("Contenu test " + UUID.randomUUID());
|
||||
msg.setType(MessageType.INDIVIDUAL);
|
||||
msg.setStatus(status);
|
||||
msg.setPriority(MessagePriority.NORMAL);
|
||||
msg.setIsEdited(false);
|
||||
msg.setIsDeleted(false);
|
||||
msg.setActif(true);
|
||||
em.persist(msg);
|
||||
em.flush();
|
||||
return msg;
|
||||
}
|
||||
|
||||
// ─── Tests ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByConversation retourne les messages actifs non supprimés")
|
||||
void findByConversation_retourneMessagesActifs() {
|
||||
Membre sender = persistMembre();
|
||||
Conversation conv = persistConversation();
|
||||
persistMessage(conv, sender, MessageStatus.SENT);
|
||||
persistMessage(conv, sender, MessageStatus.READ);
|
||||
|
||||
List<Message> messages = messageRepository.findByConversation(conv.getId(), 10);
|
||||
|
||||
assertThat(messages).isNotNull();
|
||||
assertThat(messages).hasSizeGreaterThanOrEqualTo(2);
|
||||
messages.forEach(m -> assertThat(m.getIsDeleted()).isFalse());
|
||||
@DisplayName("findMessageById retourne empty pour UUID inexistant")
|
||||
void findMessageById_inexistant_returnsEmpty() {
|
||||
Optional<Message> opt = messageRepository.findMessageById(UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByConversation respecte la limite de pagination")
|
||||
void findByConversation_respecteLimite() {
|
||||
Membre sender = persistMembre();
|
||||
Conversation conv = persistConversation();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
persistMessage(conv, sender, MessageStatus.SENT);
|
||||
}
|
||||
|
||||
List<Message> messages = messageRepository.findByConversation(conv.getId(), 3);
|
||||
|
||||
assertThat(messages).hasSizeLessThanOrEqualTo(3);
|
||||
@DisplayName("findByConversationPagine retourne liste vide pour conversation inexistante")
|
||||
void findByConversationPagine_returnsEmpty() {
|
||||
List<Message> list = messageRepository.findByConversationPagine(UUID.randomUUID(), 0, 20);
|
||||
assertThat(list).isNotNull().isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByConversation retourne liste vide pour conversation sans messages")
|
||||
void findByConversation_conversationVide_retourneListe() {
|
||||
Conversation conv = persistConversation();
|
||||
|
||||
List<Message> messages = messageRepository.findByConversation(conv.getId(), 10);
|
||||
|
||||
assertThat(messages).isNotNull();
|
||||
assertThat(messages).isEmpty();
|
||||
@DisplayName("countNonLus retourne >= 0 pour conversation inexistante")
|
||||
void countNonLus_returnsNonNegative() {
|
||||
long count = messageRepository.countNonLus(UUID.randomUUID(), UUID.randomUUID());
|
||||
assertThat(count).isGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByConversation exclut les messages supprimés")
|
||||
void findByConversation_exclutMessagesSupprimés() {
|
||||
Membre sender = persistMembre();
|
||||
Conversation conv = persistConversation();
|
||||
// Message supprimé
|
||||
Message msgSupprime = persistMessage(conv, sender, MessageStatus.SENT);
|
||||
msgSupprime.setIsDeleted(true);
|
||||
em.flush();
|
||||
|
||||
List<Message> messages = messageRepository.findByConversation(conv.getId(), 10);
|
||||
|
||||
assertThat(messages).noneMatch(m -> m.getId().equals(msgSupprime.getId()));
|
||||
@DisplayName("findActifsByConversation retourne liste vide pour conversation inexistante")
|
||||
void findActifsByConversation_returnsEmpty() {
|
||||
List<Message> list = messageRepository.findActifsByConversation(UUID.randomUUID());
|
||||
assertThat(list).isNotNull().isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("countUnreadByConversationAndMember compte les messages SENT et DELIVERED d'autres membres")
|
||||
void countUnreadByConversationAndMember_compteMsgNonLusAutresMembres() {
|
||||
Membre sender = persistMembre();
|
||||
Membre reader = persistMembre();
|
||||
Conversation conv = persistConversation();
|
||||
persistMessage(conv, sender, MessageStatus.SENT);
|
||||
persistMessage(conv, sender, MessageStatus.DELIVERED);
|
||||
persistMessage(conv, sender, MessageStatus.READ); // déjà lu → exclu
|
||||
|
||||
long count = messageRepository.countUnreadByConversationAndMember(conv.getId(), reader.getId());
|
||||
|
||||
assertThat(count).isGreaterThanOrEqualTo(2);
|
||||
@DisplayName("findDernierMessage retourne empty pour conversation inexistante")
|
||||
void findDernierMessage_returnsEmpty() {
|
||||
Optional<Message> opt = messageRepository.findDernierMessage(UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("countUnreadByConversationAndMember exclut les messages du membre lui-même")
|
||||
void countUnreadByConversationAndMember_excluMessagesPropresMembre() {
|
||||
Membre sender = persistMembre();
|
||||
Conversation conv = persistConversation();
|
||||
persistMessage(conv, sender, MessageStatus.SENT);
|
||||
|
||||
// Le sender lui-même : ses propres messages ne sont pas comptés comme non lus
|
||||
long count = messageRepository.countUnreadByConversationAndMember(conv.getId(), sender.getId());
|
||||
|
||||
assertThat(count).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("countUnreadByConversationAndMember retourne 0 pour conversation vide")
|
||||
void countUnreadByConversationAndMember_conversationVide_retourneZero() {
|
||||
Conversation conv = persistConversation();
|
||||
UUID membreId = UUID.randomUUID();
|
||||
|
||||
long count = messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId);
|
||||
|
||||
assertThat(count).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("markAllAsReadByConversationAndMember marque les messages SENT et DELIVERED en READ")
|
||||
void markAllAsReadByConversationAndMember_marqueLesMessagesEnRead() {
|
||||
Membre sender = persistMembre();
|
||||
Membre reader = persistMembre();
|
||||
Conversation conv = persistConversation();
|
||||
persistMessage(conv, sender, MessageStatus.SENT);
|
||||
persistMessage(conv, sender, MessageStatus.DELIVERED);
|
||||
|
||||
int updated = messageRepository.markAllAsReadByConversationAndMember(conv.getId(), reader.getId());
|
||||
|
||||
assertThat(updated).isGreaterThanOrEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("markAllAsReadByConversationAndMember ne touche pas les messages déjà READ")
|
||||
void markAllAsReadByConversationAndMember_neTouchePasMsgDejaRead() {
|
||||
Membre sender = persistMembre();
|
||||
Membre reader = persistMembre();
|
||||
Conversation conv = persistConversation();
|
||||
persistMessage(conv, sender, MessageStatus.READ); // déjà lu
|
||||
|
||||
int updated = messageRepository.markAllAsReadByConversationAndMember(conv.getId(), reader.getId());
|
||||
|
||||
assertThat(updated).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findLastByConversation retourne le dernier message")
|
||||
void findLastByConversation_retourneDernierMessage() {
|
||||
Membre sender = persistMembre();
|
||||
Conversation conv = persistConversation();
|
||||
// Set explicit dateCreation to guarantee ordering (PrePersist skips if non-null)
|
||||
Message premierMsg = new Message();
|
||||
premierMsg.setConversation(conv);
|
||||
premierMsg.setSender(sender);
|
||||
premierMsg.setSenderName(sender.getPrenom() + " " + sender.getNom());
|
||||
premierMsg.setContent("Premier message");
|
||||
premierMsg.setType(dev.lions.unionflow.server.api.enums.communication.MessageType.INDIVIDUAL);
|
||||
premierMsg.setStatus(MessageStatus.SENT);
|
||||
premierMsg.setPriority(dev.lions.unionflow.server.api.enums.communication.MessagePriority.NORMAL);
|
||||
premierMsg.setIsEdited(false);
|
||||
premierMsg.setIsDeleted(false);
|
||||
premierMsg.setActif(true);
|
||||
premierMsg.setDateCreation(java.time.LocalDateTime.now().minusSeconds(10));
|
||||
em.persist(premierMsg);
|
||||
em.flush();
|
||||
|
||||
Message dernierMsg = persistMessage(conv, sender, MessageStatus.DELIVERED);
|
||||
|
||||
Message last = messageRepository.findLastByConversation(conv.getId());
|
||||
|
||||
assertThat(last).isNotNull();
|
||||
assertThat(last.getId()).isEqualTo(dernierMsg.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findLastByConversation retourne null pour conversation vide")
|
||||
void findLastByConversation_conversationVide_retourneNull() {
|
||||
Conversation conv = persistConversation();
|
||||
|
||||
Message last = messageRepository.findLastByConversation(conv.getId());
|
||||
|
||||
assertThat(last).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findLastByConversation retourne null pour conversation inexistante")
|
||||
void findLastByConversation_conversationInexistante_retourneNull() {
|
||||
Message last = messageRepository.findLastByConversation(UUID.randomUUID());
|
||||
|
||||
assertThat(last).isNull();
|
||||
@DisplayName("count retourne un nombre >= 0")
|
||||
void count_returnsNonNegative() {
|
||||
assertThat(messageRepository.count()).isGreaterThanOrEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement;
|
||||
import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement;
|
||||
import dev.lions.unionflow.server.entity.Paiement;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.TestTransaction;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@QuarkusTest
|
||||
class PaiementRepositoryTest {
|
||||
|
||||
@Inject
|
||||
PaiementRepository paiementRepository;
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findById retourne null pour UUID inexistant")
|
||||
void findById_inexistant_returnsNull() {
|
||||
assertThat(paiementRepository.findById(UUID.randomUUID())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findPaiementById retourne empty pour UUID inexistant")
|
||||
void findPaiementById_inexistant_returnsEmpty() {
|
||||
Optional<Paiement> opt = paiementRepository.findPaiementById(UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByNumeroReference retourne empty pour référence inexistante")
|
||||
void findByNumeroReference_inexistant_returnsEmpty() {
|
||||
Optional<Paiement> opt = paiementRepository.findByNumeroReference("REF-" + UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("listAll retourne une liste")
|
||||
void listAll_returnsList() {
|
||||
List<Paiement> list = paiementRepository.listAll();
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("count retourne un nombre >= 0")
|
||||
void count_returnsNonNegative() {
|
||||
assertThat(paiementRepository.count()).isGreaterThanOrEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByStatut retourne une liste (vide si aucun paiement avec ce statut)")
|
||||
void findByStatut_returnsEmptyList() {
|
||||
List<Paiement> list = paiementRepository.findByStatut(StatutPaiement.EN_ATTENTE);
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByMethode retourne une liste (vide si aucun paiement avec cette méthode)")
|
||||
void findByMethode_returnsEmptyList() {
|
||||
List<Paiement> list = paiementRepository.findByMethode(MethodePaiement.VIREMENT_BANCAIRE);
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findValidesParPeriode retourne une liste pour une période donnée")
|
||||
void findValidesParPeriode_returnsEmptyList() {
|
||||
LocalDateTime debut = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime fin = LocalDateTime.now();
|
||||
List<Paiement> list = paiementRepository.findValidesParPeriode(debut, fin);
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("calculerMontantTotalValides retourne ZERO si aucun paiement validé")
|
||||
void calculerMontantTotalValides_returnsZero() {
|
||||
LocalDateTime debut = LocalDateTime.now().minusDays(1);
|
||||
LocalDateTime fin = LocalDateTime.now();
|
||||
BigDecimal total = paiementRepository.calculerMontantTotalValides(debut, fin);
|
||||
assertThat(total).isGreaterThanOrEqualTo(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByMembreId retourne une liste (vide si aucun paiement pour ce membre)")
|
||||
void findByMembreId_returnsEmptyList() {
|
||||
List<Paiement> list = paiementRepository.findByMembreId(UUID.randomUUID());
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package dev.lions.unionflow.server.repository;
|
||||
|
||||
import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement;
|
||||
import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement;
|
||||
import dev.lions.unionflow.server.entity.Versement;
|
||||
import io.quarkus.test.TestTransaction;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@QuarkusTest
|
||||
@DisplayName("VersementRepository")
|
||||
class VersementRepositoryTest {
|
||||
|
||||
@Inject
|
||||
VersementRepository versementRepository;
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findById retourne null pour UUID inexistant")
|
||||
void findById_inexistant_returnsNull() {
|
||||
assertThat(versementRepository.findById(UUID.randomUUID())).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findVersementById retourne empty pour UUID inexistant")
|
||||
void findVersementById_inexistant_returnsEmpty() {
|
||||
Optional<Versement> opt = versementRepository.findVersementById(UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByNumeroReference retourne empty pour référence inexistante")
|
||||
void findByNumeroReference_inexistant_returnsEmpty() {
|
||||
Optional<Versement> opt = versementRepository.findByNumeroReference("VRS-" + UUID.randomUUID());
|
||||
assertThat(opt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("listAll retourne une liste")
|
||||
void listAll_returnsList() {
|
||||
List<Versement> list = versementRepository.listAll();
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("count retourne un nombre >= 0")
|
||||
void count_returnsNonNegative() {
|
||||
assertThat(versementRepository.count()).isGreaterThanOrEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByMembreId retourne liste vide pour membre inexistant")
|
||||
void findByMembreId_inconnu_returnsEmpty() {
|
||||
List<Versement> list = versementRepository.findByMembreId(UUID.randomUUID());
|
||||
assertThat(list).isNotNull().isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByStatut retourne liste non nulle")
|
||||
void findByStatut_returnsNonNull() {
|
||||
List<Versement> list = versementRepository.findByStatut(StatutPaiement.EN_ATTENTE);
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findByMethode retourne liste non nulle")
|
||||
void findByMethode_returnsNonNull() {
|
||||
List<Versement> list = versementRepository.findByMethode(MethodePaiement.WAVE_MOBILE_MONEY);
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("findConfirmesParPeriode retourne liste non nulle")
|
||||
void findConfirmesParPeriode_returnsNonNull() {
|
||||
LocalDateTime debut = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime fin = LocalDateTime.now();
|
||||
List<Versement> list = versementRepository.findConfirmesParPeriode(debut, fin);
|
||||
assertThat(list).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestTransaction
|
||||
@DisplayName("calculerMontantTotalConfirmes retourne ZERO si aucun versement")
|
||||
void calculerMontantTotalConfirmes_returnsZeroWhenEmpty() {
|
||||
LocalDateTime debut = LocalDateTime.now().minusSeconds(1);
|
||||
LocalDateTime fin = LocalDateTime.now().plusSeconds(1);
|
||||
BigDecimal total = versementRepository.calculerMontantTotalConfirmes(debut, fin);
|
||||
// Peut être > 0 selon les données de test, mais ne doit pas être null
|
||||
assertThat(total).isNotNull().isGreaterThanOrEqualTo(BigDecimal.ZERO);
|
||||
}
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse;
|
||||
import dev.lions.unionflow.server.api.enums.communication.ConversationType;
|
||||
import dev.lions.unionflow.server.service.ConversationService;
|
||||
import dev.lions.unionflow.server.service.support.SecuriteHelper;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.security.TestSecurity;
|
||||
import io.restassured.http.ContentType;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests d'intégration REST pour ConversationResource.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-21
|
||||
*/
|
||||
@QuarkusTest
|
||||
class ConversationResourceTest {
|
||||
|
||||
private static final String BASE_PATH = "/api/conversations";
|
||||
private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000010";
|
||||
private static final String CONVERSATION_ID = "00000000-0000-0000-0000-000000000011";
|
||||
private static final String ORG_ID = "00000000-0000-0000-0000-000000000012";
|
||||
|
||||
@InjectMock
|
||||
ConversationService conversationService;
|
||||
|
||||
@InjectMock
|
||||
SecuriteHelper securiteHelper;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(UUID.fromString(MEMBRE_ID));
|
||||
}
|
||||
|
||||
private ConversationResponse buildConversationResponse() {
|
||||
return ConversationResponse.builder()
|
||||
.id(UUID.fromString(CONVERSATION_ID))
|
||||
.name("Discussion générale")
|
||||
.description("Conversation test")
|
||||
.type(ConversationType.GROUP)
|
||||
.participantIds(List.of(UUID.fromString(MEMBRE_ID)))
|
||||
.unreadCount(0)
|
||||
.muted(false)
|
||||
.pinned(false)
|
||||
.archived(false)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.updatedAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /api/conversations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getConversations retourne 200 avec liste vide")
|
||||
void getConversations_returnsEmptyList_200() {
|
||||
when(conversationService.getConversations(any(), any(), anyBoolean()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("$", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getConversations avec includeArchived=true retourne 200")
|
||||
void getConversations_withIncludeArchived_returns200() {
|
||||
when(conversationService.getConversations(any(), any(), eq(true)))
|
||||
.thenReturn(List.of(buildConversationResponse()));
|
||||
|
||||
given()
|
||||
.queryParam("includeArchived", true)
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getConversations avec organisationId filtre par organisation")
|
||||
void getConversations_withOrganisationId_returns200() {
|
||||
when(conversationService.getConversations(any(), eq(UUID.fromString(ORG_ID)), anyBoolean()))
|
||||
.thenReturn(List.of(buildConversationResponse()));
|
||||
|
||||
given()
|
||||
.queryParam("organisationId", ORG_ID)
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getConversations retourne 200 avec liste non vide")
|
||||
void getConversations_withResults_returns200() {
|
||||
when(conversationService.getConversations(any(), any(), anyBoolean()))
|
||||
.thenReturn(List.of(buildConversationResponse()));
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /api/conversations/{id}
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getConversationById retourne 200 quand conversation trouvée")
|
||||
void getConversationById_found_returns200() {
|
||||
when(conversationService.getConversationById(eq(UUID.fromString(CONVERSATION_ID)), any()))
|
||||
.thenReturn(buildConversationResponse());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH + "/{id}", CONVERSATION_ID)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("id", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getConversationById retourne 404 quand conversation non trouvée")
|
||||
void getConversationById_notFound_returns404() {
|
||||
when(conversationService.getConversationById(any(), any()))
|
||||
.thenThrow(new NotFoundException("Conversation non trouvée ou accès refusé"));
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH + "/{id}", UUID.randomUUID().toString())
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /api/conversations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("createConversation retourne 201 avec body valide")
|
||||
void createConversation_validRequest_returns201() {
|
||||
when(conversationService.createConversation(any(CreateConversationRequest.class), any()))
|
||||
.thenReturn(buildConversationResponse());
|
||||
|
||||
String body = """
|
||||
{
|
||||
"name": "Nouveau groupe",
|
||||
"description": "Description du groupe",
|
||||
"type": "GROUP",
|
||||
"participantIds": ["%s"]
|
||||
}
|
||||
""".formatted(MEMBRE_ID);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(201)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("createConversation avec body invalide (sans name) retourne 400")
|
||||
void createConversation_invalidRequest_returns400() {
|
||||
String body = """
|
||||
{
|
||||
"description": "Sans nom",
|
||||
"type": "GROUP",
|
||||
"participantIds": ["%s"]
|
||||
}
|
||||
""".formatted(MEMBRE_ID);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PUT /api/conversations/{id}/archive
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("archiveConversation retourne 204 quand succès")
|
||||
void archiveConversation_success_returns204() {
|
||||
doNothing().when(conversationService).archiveConversation(any(), any(), anyBoolean());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}/archive", CONVERSATION_ID)
|
||||
.then()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("archiveConversation avec archive=false retourne 204")
|
||||
void archiveConversation_unarchive_returns204() {
|
||||
doNothing().when(conversationService).archiveConversation(any(), any(), eq(false));
|
||||
|
||||
given()
|
||||
.queryParam("archive", false)
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}/archive", CONVERSATION_ID)
|
||||
.then()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("archiveConversation non trouvée retourne 404")
|
||||
void archiveConversation_notFound_returns404() {
|
||||
doThrow(new NotFoundException("Conversation non trouvée"))
|
||||
.when(conversationService).archiveConversation(any(), any(), anyBoolean());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}/archive", UUID.randomUUID().toString())
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PUT /api/conversations/{id}/mark-read
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("markAsRead retourne 204 quand succès")
|
||||
void markAsRead_success_returns204() {
|
||||
doNothing().when(conversationService).markAsRead(any(), any());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}/mark-read", CONVERSATION_ID)
|
||||
.then()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("markAsRead conversation non trouvée retourne 404")
|
||||
void markAsRead_notFound_returns404() {
|
||||
doThrow(new NotFoundException("Conversation non trouvée"))
|
||||
.when(conversationService).markAsRead(any(), any());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}/mark-read", UUID.randomUUID().toString())
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PUT /api/conversations/{id}/toggle-mute
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("toggleMute retourne 204 quand succès")
|
||||
void toggleMute_success_returns204() {
|
||||
doNothing().when(conversationService).toggleMute(any(), any());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}/toggle-mute", CONVERSATION_ID)
|
||||
.then()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("toggleMute conversation non trouvée retourne 404")
|
||||
void toggleMute_notFound_returns404() {
|
||||
doThrow(new NotFoundException("Conversation non trouvée"))
|
||||
.when(conversationService).toggleMute(any(), any());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}/toggle-mute", UUID.randomUUID().toString())
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PUT /api/conversations/{id}/toggle-pin
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("togglePin retourne 204 quand succès")
|
||||
void togglePin_success_returns204() {
|
||||
doNothing().when(conversationService).togglePin(any(), any());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}/toggle-pin", CONVERSATION_ID)
|
||||
.then()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("togglePin conversation non trouvée retourne 404")
|
||||
void togglePin_notFound_returns404() {
|
||||
doThrow(new NotFoundException("Conversation non trouvée"))
|
||||
.when(conversationService).togglePin(any(), any());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}/toggle-pin", UUID.randomUUID().toString())
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Sécurité — non authentifié
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("getConversations sans authentification retourne 401")
|
||||
void getConversations_unauthenticated_returns401() {
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest;
|
||||
import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
|
||||
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 dev.lions.unionflow.server.service.MessageService;
|
||||
import dev.lions.unionflow.server.service.support.SecuriteHelper;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.security.TestSecurity;
|
||||
import io.restassured.http.ContentType;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests d'intégration REST pour MessageResource.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-21
|
||||
*/
|
||||
@QuarkusTest
|
||||
class MessageResourceTest {
|
||||
|
||||
private static final String BASE_PATH = "/api/messages";
|
||||
private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000020";
|
||||
private static final String MESSAGE_ID = "00000000-0000-0000-0000-000000000021";
|
||||
private static final String CONVERSATION_ID = "00000000-0000-0000-0000-000000000022";
|
||||
|
||||
@InjectMock
|
||||
MessageService messageService;
|
||||
|
||||
@InjectMock
|
||||
SecuriteHelper securiteHelper;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
when(securiteHelper.resolveMembreId()).thenReturn(UUID.fromString(MEMBRE_ID));
|
||||
}
|
||||
|
||||
private MessageResponse buildMessageResponse() {
|
||||
return MessageResponse.builder()
|
||||
.id(UUID.fromString(MESSAGE_ID))
|
||||
.conversationId(UUID.fromString(CONVERSATION_ID))
|
||||
.senderId(UUID.fromString(MEMBRE_ID))
|
||||
.senderName("Alice Martin")
|
||||
.content("Bonjour !")
|
||||
.type(MessageType.INDIVIDUAL)
|
||||
.status(MessageStatus.SENT)
|
||||
.priority(MessagePriority.NORMAL)
|
||||
.edited(false)
|
||||
.deleted(false)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.build();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /api/messages
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMessages sans conversationId retourne 400")
|
||||
void getMessages_missingConversationId_returns400() {
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMessages avec conversationId valide retourne 200")
|
||||
void getMessages_withConversationId_returns200() {
|
||||
when(messageService.getMessages(eq(UUID.fromString(CONVERSATION_ID)), any(), anyInt()))
|
||||
.thenReturn(List.of(buildMessageResponse()));
|
||||
|
||||
given()
|
||||
.queryParam("conversationId", CONVERSATION_ID)
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("$", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMessages avec conversationId valide retourne liste vide")
|
||||
void getMessages_emptyConversation_returns200() {
|
||||
when(messageService.getMessages(any(), any(), anyInt()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.queryParam("conversationId", CONVERSATION_ID)
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMessages avec limit personnalisé retourne 200")
|
||||
void getMessages_withLimit_returns200() {
|
||||
when(messageService.getMessages(any(), any(), eq(10)))
|
||||
.thenReturn(List.of(buildMessageResponse()));
|
||||
|
||||
given()
|
||||
.queryParam("conversationId", CONVERSATION_ID)
|
||||
.queryParam("limit", 10)
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMessages conversation non trouvée retourne 404")
|
||||
void getMessages_conversationNotFound_returns404() {
|
||||
when(messageService.getMessages(any(), any(), anyInt()))
|
||||
.thenThrow(new NotFoundException("Conversation non trouvée ou accès refusé"));
|
||||
|
||||
given()
|
||||
.queryParam("conversationId", CONVERSATION_ID)
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /api/messages
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("sendMessage avec body valide retourne 201")
|
||||
void sendMessage_validRequest_returns201() {
|
||||
when(messageService.sendMessage(any(SendMessageRequest.class), any()))
|
||||
.thenReturn(buildMessageResponse());
|
||||
|
||||
String body = """
|
||||
{
|
||||
"conversationId": "%s",
|
||||
"content": "Bonjour à tous !"
|
||||
}
|
||||
""".formatted(CONVERSATION_ID);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(201)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("id", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("sendMessage sans conversationId retourne 400")
|
||||
void sendMessage_missingConversationId_returns400() {
|
||||
String body = """
|
||||
{
|
||||
"content": "Message sans conversation"
|
||||
}
|
||||
""";
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("sendMessage sans contenu retourne 400")
|
||||
void sendMessage_missingContent_returns400() {
|
||||
String body = """
|
||||
{
|
||||
"conversationId": "%s"
|
||||
}
|
||||
""".formatted(CONVERSATION_ID);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("sendMessage conversation non trouvée retourne 404")
|
||||
void sendMessage_conversationNotFound_returns404() {
|
||||
when(messageService.sendMessage(any(), any()))
|
||||
.thenThrow(new NotFoundException("Conversation non trouvée ou accès refusé"));
|
||||
|
||||
String body = """
|
||||
{
|
||||
"conversationId": "%s",
|
||||
"content": "Test"
|
||||
}
|
||||
""".formatted(CONVERSATION_ID);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PUT /api/messages/{id}
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("editMessage avec body valide retourne 200")
|
||||
void editMessage_validRequest_returns200() {
|
||||
when(messageService.editMessage(any(), any(), anyString()))
|
||||
.thenReturn(buildMessageResponse());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"content\": \"Message modifié\"}")
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}", MESSAGE_ID)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("editMessage sans contenu retourne 400")
|
||||
void editMessage_missingContent_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{}")
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}", MESSAGE_ID)
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("editMessage avec contenu vide retourne 400")
|
||||
void editMessage_emptyContent_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"content\": \"\"}")
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}", MESSAGE_ID)
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("editMessage message non trouvé retourne 404")
|
||||
void editMessage_notFound_returns404() {
|
||||
when(messageService.editMessage(any(), any(), anyString()))
|
||||
.thenThrow(new NotFoundException("Message non trouvé"));
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"content\": \"Contenu modifié\"}")
|
||||
.when()
|
||||
.put(BASE_PATH + "/{id}", UUID.randomUUID().toString())
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DELETE /api/messages/{id}
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("deleteMessage retourne 204 quand succès")
|
||||
void deleteMessage_success_returns204() {
|
||||
doNothing().when(messageService).deleteMessage(any(), any());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.delete(BASE_PATH + "/{id}", MESSAGE_ID)
|
||||
.then()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestSecurity(user = "alice@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("deleteMessage message non trouvé retourne 404")
|
||||
void deleteMessage_notFound_returns404() {
|
||||
doThrow(new NotFoundException("Message non trouvé"))
|
||||
.when(messageService).deleteMessage(any(), any());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.delete(BASE_PATH + "/{id}", UUID.randomUUID().toString())
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Sécurité — non authentifié
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("getMessages sans authentification retourne 401")
|
||||
void getMessages_unauthenticated_returns401() {
|
||||
given()
|
||||
.queryParam("conversationId", CONVERSATION_ID)
|
||||
.when()
|
||||
.get(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sendMessage sans authentification retourne 401")
|
||||
void sendMessage_unauthenticated_returns401() {
|
||||
String body = """
|
||||
{
|
||||
"conversationId": "%s",
|
||||
"content": "Message non auth"
|
||||
}
|
||||
""".formatted(CONVERSATION_ID);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(401);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse;
|
||||
import dev.lions.unionflow.server.service.MessagingService;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.security.TestSecurity;
|
||||
import io.restassured.http.ContentType;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
* Tests d'intégration REST pour {@link MessagingResource}.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@QuarkusTest
|
||||
@DisplayName("MessagingResource")
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class MessagingResourceTest {
|
||||
|
||||
private static final String BASE = "/api/messagerie";
|
||||
private static final String CONV_ID = "00000000-0000-0000-0000-000000000001";
|
||||
private static final String MSG_ID = "00000000-0000-0000-0000-000000000002";
|
||||
private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000003";
|
||||
private static final String ORG_ID = "00000000-0000-0000-0000-000000000004";
|
||||
|
||||
@InjectMock
|
||||
MessagingService messagingService;
|
||||
|
||||
// ── POST /conversations/directe ───────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("demarrerConversationDirecte — requête valide → 201")
|
||||
void demarrerConversationDirecte_valid_returns201() {
|
||||
ConversationResponse conv = ConversationResponse.builder()
|
||||
.id(UUID.fromString(CONV_ID))
|
||||
.typeConversation("DIRECTE")
|
||||
.statut("ACTIVE")
|
||||
.build();
|
||||
when(messagingService.demarrerConversationDirecte(any())).thenReturn(conv);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"destinataireId\":\"" + MEMBRE_ID + "\",\"organisationId\":\"" + ORG_ID + "\"}")
|
||||
.when().post(BASE + "/conversations/directe")
|
||||
.then().statusCode(201)
|
||||
.body("typeConversation", equalTo("DIRECTE"))
|
||||
.body("statut", equalTo("ACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("demarrerConversationDirecte — destinataireId manquant → 400")
|
||||
void demarrerConversationDirecte_missingDestinataire_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"organisationId\":\"" + ORG_ID + "\"}")
|
||||
.when().post(BASE + "/conversations/directe")
|
||||
.then().statusCode(400);
|
||||
}
|
||||
|
||||
// ── POST /conversations/role ──────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("demarrerConversationRole — requête valide → 201")
|
||||
void demarrerConversationRole_valid_returns201() {
|
||||
ConversationResponse conv = ConversationResponse.builder()
|
||||
.id(UUID.fromString(CONV_ID))
|
||||
.typeConversation("ROLE_CANAL")
|
||||
.roleCible("TRESORIER")
|
||||
.build();
|
||||
when(messagingService.demarrerConversationRole(any())).thenReturn(conv);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"organisationId\":\"" + ORG_ID + "\","
|
||||
+ "\"roleCible\":\"TRESORIER\","
|
||||
+ "\"contenuInitial\":\"Bonjour\"}")
|
||||
.when().post(BASE + "/conversations/role")
|
||||
.then().statusCode(201)
|
||||
.body("typeConversation", equalTo("ROLE_CANAL"))
|
||||
.body("roleCible", equalTo("TRESORIER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("demarrerConversationRole — rôle invalide → 400")
|
||||
void demarrerConversationRole_invalidRole_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"organisationId\":\"" + ORG_ID + "\","
|
||||
+ "\"roleCible\":\"DIRECTEUR\","
|
||||
+ "\"contenuInitial\":\"Bonjour\"}")
|
||||
.when().post(BASE + "/conversations/role")
|
||||
.then().statusCode(400);
|
||||
}
|
||||
|
||||
// ── GET /conversations ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMesConversations — liste vide → 200")
|
||||
void getMesConversations_empty_returns200() {
|
||||
when(messagingService.getMesConversations()).thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/conversations")
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMesConversations — liste non vide → 200")
|
||||
void getMesConversations_nonEmpty_returns200() {
|
||||
ConversationSummaryResponse summary = ConversationSummaryResponse.builder()
|
||||
.id(UUID.fromString(CONV_ID))
|
||||
.typeConversation("DIRECTE")
|
||||
.titre("Alice Dupont")
|
||||
.build();
|
||||
when(messagingService.getMesConversations()).thenReturn(List.of(summary));
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/conversations")
|
||||
.then().statusCode(200)
|
||||
.body("[0].titre", equalTo("Alice Dupont"));
|
||||
}
|
||||
|
||||
// ── GET /conversations/{id} ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getConversation — trouvée → 200")
|
||||
void getConversation_found_returns200() {
|
||||
ConversationResponse conv = ConversationResponse.builder()
|
||||
.id(UUID.fromString(CONV_ID))
|
||||
.typeConversation("DIRECTE")
|
||||
.titre("Bob Martin")
|
||||
.build();
|
||||
when(messagingService.getConversation(any(UUID.class))).thenReturn(conv);
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/conversations/" + CONV_ID)
|
||||
.then().statusCode(200)
|
||||
.body("titre", equalTo("Bob Martin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(8)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getConversation — non trouvée → 404")
|
||||
void getConversation_notFound_returns404() {
|
||||
when(messagingService.getConversation(any(UUID.class)))
|
||||
.thenThrow(new NotFoundException("conversation non trouvée"));
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/conversations/" + CONV_ID)
|
||||
.then().statusCode(404);
|
||||
}
|
||||
|
||||
// ── DELETE /conversations/{id} ────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(9)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("archiverConversation → 200")
|
||||
void archiverConversation_returns200() {
|
||||
ConversationResponse conv = ConversationResponse.builder()
|
||||
.id(UUID.fromString(CONV_ID))
|
||||
.statut("ARCHIVEE")
|
||||
.build();
|
||||
when(messagingService.archiverConversation(any(UUID.class))).thenReturn(conv);
|
||||
|
||||
given()
|
||||
.when().delete(BASE + "/conversations/" + CONV_ID)
|
||||
.then().statusCode(200)
|
||||
.body("statut", equalTo("ARCHIVEE"));
|
||||
}
|
||||
|
||||
// ── POST /conversations/{id}/messages ─────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(10)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("envoyerMessage — message texte → 201")
|
||||
void envoyerMessage_texte_returns201() {
|
||||
MessageResponse msg = MessageResponse.builder()
|
||||
.id(UUID.fromString(MSG_ID))
|
||||
.typeMessage("TEXTE")
|
||||
.contenu("Bonjour !")
|
||||
.build();
|
||||
when(messagingService.envoyerMessage(any(UUID.class), any())).thenReturn(msg);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"typeMessage\":\"TEXTE\",\"contenu\":\"Bonjour !\"}")
|
||||
.when().post(BASE + "/conversations/" + CONV_ID + "/messages")
|
||||
.then().statusCode(201)
|
||||
.body("typeMessage", equalTo("TEXTE"))
|
||||
.body("contenu", equalTo("Bonjour !"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(11)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("envoyerMessage — type invalide → 400")
|
||||
void envoyerMessage_invalidType_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"typeMessage\":\"VIDEO\",\"contenu\":\"test\"}")
|
||||
.when().post(BASE + "/conversations/" + CONV_ID + "/messages")
|
||||
.then().statusCode(400);
|
||||
}
|
||||
|
||||
// ── GET /conversations/{id}/messages ──────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(12)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMessages — page 0 → 200")
|
||||
void getMessages_page0_returns200() {
|
||||
when(messagingService.getMessages(any(UUID.class), anyInt()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/conversations/" + CONV_ID + "/messages")
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(13)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMessages — page custom → 200")
|
||||
void getMessages_customPage_returns200() {
|
||||
when(messagingService.getMessages(any(UUID.class), anyInt()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.queryParam("page", 2)
|
||||
.when().get(BASE + "/conversations/" + CONV_ID + "/messages")
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
// ── PUT /conversations/{id}/lire ──────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(14)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("marquerLu → 204")
|
||||
void marquerLu_returns204() {
|
||||
doNothing().when(messagingService).marquerConversationLue(any(UUID.class));
|
||||
|
||||
given()
|
||||
.when().put(BASE + "/conversations/" + CONV_ID + "/lire")
|
||||
.then().statusCode(204);
|
||||
}
|
||||
|
||||
// ── DELETE /conversations/{cId}/messages/{mId} ────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(15)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("supprimerMessage → 204")
|
||||
void supprimerMessage_returns204() {
|
||||
doNothing().when(messagingService).supprimerMessage(any(UUID.class), any(UUID.class));
|
||||
|
||||
given()
|
||||
.when().delete(BASE + "/conversations/" + CONV_ID + "/messages/" + MSG_ID)
|
||||
.then().statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(16)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("supprimerMessage — non trouvé → 404")
|
||||
void supprimerMessage_notFound_returns404() {
|
||||
org.mockito.Mockito.doThrow(new NotFoundException("message introuvable"))
|
||||
.when(messagingService).supprimerMessage(any(UUID.class), any(UUID.class));
|
||||
|
||||
given()
|
||||
.when().delete(BASE + "/conversations/" + CONV_ID + "/messages/" + MSG_ID)
|
||||
.then().statusCode(404);
|
||||
}
|
||||
|
||||
// ── POST /blocages ────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(17)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("bloquerMembre — requête valide → 204")
|
||||
void bloquerMembre_valid_returns204() {
|
||||
doNothing().when(messagingService).bloquerMembre(any());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"membreABloquerId\":\"" + MEMBRE_ID + "\","
|
||||
+ "\"organisationId\":\"" + ORG_ID + "\"}")
|
||||
.when().post(BASE + "/blocages")
|
||||
.then().statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(18)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("bloquerMembre — membreABloquerId manquant → 400")
|
||||
void bloquerMembre_missingId_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"organisationId\":\"" + ORG_ID + "\"}")
|
||||
.when().post(BASE + "/blocages")
|
||||
.then().statusCode(400);
|
||||
}
|
||||
|
||||
// ── DELETE /blocages/{membreId} ───────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(19)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("debloquerMembre → 204")
|
||||
void debloquerMembre_returns204() {
|
||||
doNothing().when(messagingService).debloquerMembre(any(UUID.class), any());
|
||||
|
||||
given()
|
||||
.queryParam("organisationId", ORG_ID)
|
||||
.when().delete(BASE + "/blocages/" + MEMBRE_ID)
|
||||
.then().statusCode(204);
|
||||
}
|
||||
|
||||
// ── GET /blocages ─────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(20)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getMesBlocages → 200")
|
||||
void getMesBlocages_returns200() {
|
||||
when(messagingService.getMesBlocages()).thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/blocages")
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
// ── GET /politique/{organisationId} ───────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(21)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("getPolitique → 200")
|
||||
void getPolitique_returns200() {
|
||||
ContactPolicyResponse policy = ContactPolicyResponse.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.organisationId(UUID.fromString(ORG_ID))
|
||||
.typePolitique("OUVERT")
|
||||
.autoriserMembreVersMembre(true)
|
||||
.autoriserMembreVersRole(true)
|
||||
.autoriserNotesVocales(true)
|
||||
.build();
|
||||
when(messagingService.getPolitique(any(UUID.class))).thenReturn(policy);
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/politique/" + ORG_ID)
|
||||
.then().statusCode(200)
|
||||
.body("typePolitique", equalTo("OUVERT"))
|
||||
.body("autoriserNotesVocales", equalTo(true));
|
||||
}
|
||||
|
||||
// ── PUT /politique/{organisationId} ───────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(22)
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
@DisplayName("mettreAJourPolitique — ADMIN → 200")
|
||||
void mettreAJourPolitique_admin_returns200() {
|
||||
ContactPolicyResponse updated = ContactPolicyResponse.builder()
|
||||
.typePolitique("BUREAU_SEULEMENT")
|
||||
.build();
|
||||
when(messagingService.mettreAJourPolitique(any(UUID.class), any())).thenReturn(updated);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"typePolitique\":\"BUREAU_SEULEMENT\"}")
|
||||
.when().put(BASE + "/politique/" + ORG_ID)
|
||||
.then().statusCode(200)
|
||||
.body("typePolitique", equalTo("BUREAU_SEULEMENT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(23)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
@DisplayName("mettreAJourPolitique — MEMBRE → 403")
|
||||
void mettreAJourPolitique_membre_returns403() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"typePolitique\":\"BUREAU_SEULEMENT\"}")
|
||||
.when().put(BASE + "/politique/" + ORG_ID)
|
||||
.then().statusCode(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(24)
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
@DisplayName("mettreAJourPolitique — politique invalide → 400")
|
||||
void mettreAJourPolitique_invalidPolicy_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"typePolitique\":\"LIBRE\"}")
|
||||
.when().put(BASE + "/politique/" + ORG_ID)
|
||||
.then().statusCode(400);
|
||||
}
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.anyOf;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementGatewayResponse;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementResponse;
|
||||
import dev.lions.unionflow.server.api.dto.paiement.response.PaiementSummaryResponse;
|
||||
import dev.lions.unionflow.server.service.PaiementService;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.security.TestSecurity;
|
||||
import io.restassured.http.ContentType;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
* Tests d'intégration REST pour PaiementResource.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 2.0
|
||||
* @since 2026-03-21
|
||||
*/
|
||||
@QuarkusTest
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class PaiementResourceTest {
|
||||
|
||||
private static final String BASE_PATH = "/api/paiements";
|
||||
private static final String PAIEMENT_ID = "00000000-0000-0000-0000-000000000010";
|
||||
private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000020";
|
||||
|
||||
@InjectMock
|
||||
PaiementService paiementService;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /api/paiements/{id}
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void trouverParId_found_returns200() {
|
||||
PaiementResponse response = PaiementResponse.builder()
|
||||
.numeroReference("PAY-001")
|
||||
.build();
|
||||
when(paiementService.trouverParId(any(UUID.class))).thenReturn(response);
|
||||
|
||||
given()
|
||||
.pathParam("id", PAIEMENT_ID)
|
||||
.when()
|
||||
.get(BASE_PATH + "/{id}")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("$", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void trouverParId_notFound_returns404() {
|
||||
when(paiementService.trouverParId(any(UUID.class)))
|
||||
.thenThrow(new NotFoundException("Paiement non trouvé"));
|
||||
|
||||
given()
|
||||
.pathParam("id", UUID.randomUUID())
|
||||
.when()
|
||||
.get(BASE_PATH + "/{id}")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /api/paiements/reference/{numeroReference}
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void trouverParNumeroReference_found_returns200() {
|
||||
PaiementResponse response = PaiementResponse.builder()
|
||||
.numeroReference("PAY-REF-001")
|
||||
.build();
|
||||
when(paiementService.trouverParNumeroReference(anyString())).thenReturn(response);
|
||||
|
||||
given()
|
||||
.pathParam("numeroReference", "PAY-REF-001")
|
||||
.when()
|
||||
.get(BASE_PATH + "/reference/{numeroReference}")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void trouverParNumeroReference_notFound_returns404() {
|
||||
when(paiementService.trouverParNumeroReference(anyString()))
|
||||
.thenThrow(new NotFoundException("Référence non trouvée"));
|
||||
|
||||
given()
|
||||
.pathParam("numeroReference", "PAY-INEXISTANT-99999")
|
||||
.when()
|
||||
.get(BASE_PATH + "/reference/{numeroReference}")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /api/paiements/membre/{membreId}
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void listerParMembre_returns200() {
|
||||
when(paiementService.listerParMembre(any(UUID.class)))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.pathParam("membreId", MEMBRE_ID)
|
||||
.when()
|
||||
.get(BASE_PATH + "/membre/{membreId}")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("$", notNullValue());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /api/paiements/mes-paiements/historique
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
void getMonHistoriquePaiements_returns200() {
|
||||
List<PaiementSummaryResponse> history = Collections.emptyList();
|
||||
when(paiementService.getMonHistoriquePaiements(anyInt())).thenReturn(history);
|
||||
|
||||
given()
|
||||
.queryParam("limit", 5)
|
||||
.when()
|
||||
.get(BASE_PATH + "/mes-paiements/historique")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("$", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
void getMonHistoriquePaiements_defaultLimit_returns200() {
|
||||
when(paiementService.getMonHistoriquePaiements(anyInt())).thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.when()
|
||||
.get(BASE_PATH + "/mes-paiements/historique")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /api/paiements
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(8)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void creerPaiement_success_returns201() {
|
||||
PaiementResponse response = PaiementResponse.builder()
|
||||
.numeroReference("PAY-NEW-001")
|
||||
.build();
|
||||
when(paiementService.creerPaiement(any())).thenReturn(response);
|
||||
|
||||
String body = String.format("""
|
||||
{
|
||||
"membreId": "%s",
|
||||
"montant": 10000,
|
||||
"numeroReference": "PAY-NEW-001",
|
||||
"methodePaiement": "ESPECES",
|
||||
"codeDevise": "XOF"
|
||||
}
|
||||
""", MEMBRE_ID);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(anyOf(equalTo(201), equalTo(400)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(9)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void creerPaiement_serverError_returns500() {
|
||||
when(paiementService.creerPaiement(any()))
|
||||
.thenThrow(new RuntimeException("db error"));
|
||||
|
||||
String body = String.format("""
|
||||
{"membreId": "%s", "montant": 10000, "numeroReference": "PAY-ERR", "methodePaiement": "ESPECES", "codeDevise": "XOF"}
|
||||
""", MEMBRE_ID);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH)
|
||||
.then()
|
||||
.statusCode(anyOf(equalTo(500), equalTo(400)));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /api/paiements/{id}/valider
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(10)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void validerPaiement_success_returns200() {
|
||||
PaiementResponse response = PaiementResponse.builder()
|
||||
.numeroReference("PAY-001")
|
||||
.build();
|
||||
when(paiementService.validerPaiement(any(UUID.class))).thenReturn(response);
|
||||
|
||||
given()
|
||||
.pathParam("id", PAIEMENT_ID)
|
||||
.contentType(ContentType.JSON)
|
||||
.when()
|
||||
.post(BASE_PATH + "/{id}/valider")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(11)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void validerPaiement_notFound_returns404() {
|
||||
when(paiementService.validerPaiement(any(UUID.class)))
|
||||
.thenThrow(new NotFoundException("Paiement non trouvé"));
|
||||
|
||||
given()
|
||||
.pathParam("id", UUID.randomUUID())
|
||||
.contentType(ContentType.JSON)
|
||||
.when()
|
||||
.post(BASE_PATH + "/{id}/valider")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /api/paiements/{id}/annuler
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(12)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void annulerPaiement_success_returns200() {
|
||||
PaiementResponse response = PaiementResponse.builder()
|
||||
.numeroReference("PAY-001")
|
||||
.build();
|
||||
when(paiementService.annulerPaiement(any(UUID.class))).thenReturn(response);
|
||||
|
||||
given()
|
||||
.pathParam("id", PAIEMENT_ID)
|
||||
.contentType(ContentType.JSON)
|
||||
.when()
|
||||
.post(BASE_PATH + "/{id}/annuler")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(13)
|
||||
@TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"})
|
||||
void annulerPaiement_notFound_returns404() {
|
||||
when(paiementService.annulerPaiement(any(UUID.class)))
|
||||
.thenThrow(new NotFoundException("Paiement non trouvé"));
|
||||
|
||||
given()
|
||||
.pathParam("id", UUID.randomUUID())
|
||||
.contentType(ContentType.JSON)
|
||||
.when()
|
||||
.post(BASE_PATH + "/{id}/annuler")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /api/paiements/initier-paiement-en-ligne
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(14)
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
void initierPaiementEnLigne_success_returns201() {
|
||||
PaiementGatewayResponse response = PaiementGatewayResponse.builder()
|
||||
.transactionId(UUID.randomUUID())
|
||||
.redirectUrl("https://wave.example.com/pay/TXN-001")
|
||||
.build();
|
||||
when(paiementService.initierPaiementEnLigne(any())).thenReturn(response);
|
||||
|
||||
String body = String.format("""
|
||||
{
|
||||
"cotisationId": "%s",
|
||||
"methodePaiement": "WAVE",
|
||||
"numeroTelephone": "771234567"
|
||||
}
|
||||
""", UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH + "/initier-paiement-en-ligne")
|
||||
.then()
|
||||
.statusCode(anyOf(equalTo(201), equalTo(400)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(15)
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
void initierPaiementEnLigne_serverError_returns500() {
|
||||
when(paiementService.initierPaiementEnLigne(any()))
|
||||
.thenThrow(new RuntimeException("gateway error"));
|
||||
|
||||
String body = String.format("""
|
||||
{"cotisationId": "%s", "methodePaiement": "WAVE", "numeroTelephone": "771234567"}
|
||||
""", UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH + "/initier-paiement-en-ligne")
|
||||
.then()
|
||||
.statusCode(anyOf(equalTo(500), equalTo(400)));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /api/paiements/initier-depot-epargne-en-ligne
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(16)
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
void initierDepotEpargneEnLigne_success_returns201() {
|
||||
PaiementGatewayResponse response = PaiementGatewayResponse.builder()
|
||||
.transactionId(UUID.randomUUID())
|
||||
.redirectUrl("https://wave.example.com/pay/DEPOT-001")
|
||||
.build();
|
||||
when(paiementService.initierDepotEpargneEnLigne(any())).thenReturn(response);
|
||||
|
||||
String body = String.format("""
|
||||
{
|
||||
"compteId": "%s",
|
||||
"montant": 10000,
|
||||
"numeroTelephone": "771234567"
|
||||
}
|
||||
""", UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH + "/initier-depot-epargne-en-ligne")
|
||||
.then()
|
||||
.statusCode(anyOf(equalTo(201), equalTo(400)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(17)
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
void initierDepotEpargneEnLigne_serverError_returns500() {
|
||||
when(paiementService.initierDepotEpargneEnLigne(any()))
|
||||
.thenThrow(new RuntimeException("epargne error"));
|
||||
|
||||
String body = String.format("""
|
||||
{"compteId": "%s", "montant": 10000, "numeroTelephone": "771234567"}
|
||||
""", UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH + "/initier-depot-epargne-en-ligne")
|
||||
.then()
|
||||
.statusCode(anyOf(equalTo(500), equalTo(400)));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /api/paiements/declarer-paiement-manuel
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@Order(18)
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
void declarerPaiementManuel_success_returns201() {
|
||||
PaiementResponse response = PaiementResponse.builder()
|
||||
.numeroReference("MANUEL-001")
|
||||
.build();
|
||||
when(paiementService.declarerPaiementManuel(any())).thenReturn(response);
|
||||
|
||||
String body = String.format("""
|
||||
{
|
||||
"cotisationId": "%s",
|
||||
"methodePaiement": "ESPECES",
|
||||
"montant": 5000,
|
||||
"dateDeclaration": "2026-03-21",
|
||||
"commentaire": "Paiement remis en main propre"
|
||||
}
|
||||
""", UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH + "/declarer-paiement-manuel")
|
||||
.then()
|
||||
.statusCode(201)
|
||||
.contentType(ContentType.JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(19)
|
||||
@TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"})
|
||||
void declarerPaiementManuel_serverError_returns500() {
|
||||
when(paiementService.declarerPaiementManuel(any()))
|
||||
.thenThrow(new RuntimeException("declaration error"));
|
||||
|
||||
String body = String.format("""
|
||||
{"cotisationId": "%s", "methodePaiement": "ESPECES", "montant": 5000}
|
||||
""", UUID.randomUUID());
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(body)
|
||||
.when()
|
||||
.post(BASE_PATH + "/declarer-paiement-manuel")
|
||||
.then()
|
||||
.statusCode(500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package dev.lions.unionflow.server.resource;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementGatewayResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementStatutResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementSummaryResponse;
|
||||
import dev.lions.unionflow.server.service.VersementService;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.security.TestSecurity;
|
||||
import io.restassured.http.ContentType;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
* Tests d'intégration REST pour {@link VersementResource}.
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 4.0
|
||||
* @since 2026-04-13
|
||||
*/
|
||||
@QuarkusTest
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
class VersementResourceTest {
|
||||
|
||||
private static final String BASE = "/api/versements";
|
||||
private static final String VERSEMENT_ID = "00000000-0000-0000-0000-000000000010";
|
||||
private static final String MEMBRE_ID = "00000000-0000-0000-0000-000000000020";
|
||||
private static final String INTENTION_ID = "00000000-0000-0000-0000-000000000030";
|
||||
|
||||
@InjectMock
|
||||
VersementService versementService;
|
||||
|
||||
// ── GET /{id} ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void trouverParId_found_returns200() {
|
||||
VersementResponse response = VersementResponse.builder()
|
||||
.numeroReference("VRS-2026-001")
|
||||
.build();
|
||||
when(versementService.trouverParId(any(UUID.class))).thenReturn(response);
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/" + VERSEMENT_ID)
|
||||
.then().statusCode(200)
|
||||
.body("numeroReference", equalTo("VRS-2026-001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void trouverParId_notFound_returns404() {
|
||||
when(versementService.trouverParId(any(UUID.class)))
|
||||
.thenThrow(new NotFoundException("non trouvé"));
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/" + VERSEMENT_ID)
|
||||
.then().statusCode(404);
|
||||
}
|
||||
|
||||
// ── GET /reference/{ref} ──────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void trouverParReference_found_returns200() {
|
||||
VersementResponse response = VersementResponse.builder()
|
||||
.numeroReference("VRS-2026-001")
|
||||
.build();
|
||||
when(versementService.trouverParNumeroReference(anyString())).thenReturn(response);
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/reference/VRS-2026-001")
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
// ── GET /membre/{id} ──────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void listerParMembre_returns200() {
|
||||
when(versementService.listerParMembre(any(UUID.class)))
|
||||
.thenReturn(List.of(new VersementSummaryResponse()));
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/membre/" + MEMBRE_ID)
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
// ── GET /mes-versements ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void getMesVersements_returns200() {
|
||||
when(versementService.getMesVersements(anyInt()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/mes-versements")
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void getMesVersements_customLimit_returns200() {
|
||||
when(versementService.getMesVersements(10))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
given()
|
||||
.queryParam("limit", 10)
|
||||
.when().get(BASE + "/mes-versements")
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
// ── POST /{id}/valider ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void validerVersement_returns200() {
|
||||
VersementResponse response = VersementResponse.builder()
|
||||
.statutPaiement("CONFIRME")
|
||||
.build();
|
||||
when(versementService.validerVersement(any(UUID.class))).thenReturn(response);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.when().post(BASE + "/" + VERSEMENT_ID + "/valider")
|
||||
.then().statusCode(200)
|
||||
.body("statutPaiement", equalTo("CONFIRME"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(8)
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void validerVersement_notFound_returns404() {
|
||||
when(versementService.validerVersement(any(UUID.class)))
|
||||
.thenThrow(new NotFoundException("non trouvé"));
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.when().post(BASE + "/" + VERSEMENT_ID + "/valider")
|
||||
.then().statusCode(404);
|
||||
}
|
||||
|
||||
// ── POST /{id}/annuler ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(9)
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void annulerVersement_returns200() {
|
||||
VersementResponse response = VersementResponse.builder()
|
||||
.statutPaiement("ANNULE")
|
||||
.build();
|
||||
when(versementService.annulerVersement(any(UUID.class))).thenReturn(response);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.when().post(BASE + "/" + VERSEMENT_ID + "/annuler")
|
||||
.then().statusCode(200);
|
||||
}
|
||||
|
||||
// ── POST /initier-wave ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(10)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierWave_validRequest_returns201() {
|
||||
VersementGatewayResponse gateway = VersementGatewayResponse.builder()
|
||||
.versementId(UUID.fromString(VERSEMENT_ID))
|
||||
.waveLaunchUrl("wave://checkout/abc123")
|
||||
.waveCheckoutSessionId("cos-abc")
|
||||
.clientReference(INTENTION_ID)
|
||||
.montant(new BigDecimal("5000"))
|
||||
.statut("EN_ATTENTE")
|
||||
.build();
|
||||
when(versementService.initierVersementWave(any())).thenReturn(gateway);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"cotisationId\":\"" + UUID.randomUUID() + "\",\"numeroTelephone\":\"771234567\"}")
|
||||
.when().post(BASE + "/initier-wave")
|
||||
.then().statusCode(201)
|
||||
.body("waveLaunchUrl", equalTo("wave://checkout/abc123"))
|
||||
.body("waveCheckoutSessionId", equalTo("cos-abc"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(11)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierWave_missingCotisationId_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"numeroTelephone\":\"771234567\"}")
|
||||
.when().post(BASE + "/initier-wave")
|
||||
.then().statusCode(400);
|
||||
}
|
||||
|
||||
// ── GET /statut/{intentionId} ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(12)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void getStatutVersement_completee_returns200() {
|
||||
VersementStatutResponse statut = VersementStatutResponse.builder()
|
||||
.intentionId(UUID.fromString(INTENTION_ID))
|
||||
.statut("COMPLETEE")
|
||||
.confirme(true)
|
||||
.message("Versement confirmé !")
|
||||
.build();
|
||||
when(versementService.verifierStatutVersement(any(UUID.class))).thenReturn(statut);
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/statut/" + INTENTION_ID)
|
||||
.then().statusCode(200)
|
||||
.body("confirme", equalTo(true))
|
||||
.body("statut", equalTo("COMPLETEE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(13)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void getStatutVersement_notFound_returns404() {
|
||||
when(versementService.verifierStatutVersement(any(UUID.class)))
|
||||
.thenThrow(new NotFoundException("non trouvé"));
|
||||
|
||||
given()
|
||||
.when().get(BASE + "/statut/" + INTENTION_ID)
|
||||
.then().statusCode(404);
|
||||
}
|
||||
|
||||
// ── POST /declarer-manuel ─────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(14)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void declarerManuel_validRequest_returns201() {
|
||||
VersementResponse response = VersementResponse.builder()
|
||||
.statutPaiement("EN_ATTENTE_VALIDATION")
|
||||
.methodePaiement("ESPECES")
|
||||
.build();
|
||||
when(versementService.declarerVersementManuel(any())).thenReturn(response);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"cotisationId\":\"" + UUID.randomUUID()
|
||||
+ "\",\"methodePaiement\":\"ESPECES\"}")
|
||||
.when().post(BASE + "/declarer-manuel")
|
||||
.then().statusCode(201)
|
||||
.body("statutPaiement", equalTo("EN_ATTENTE_VALIDATION"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(15)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void declarerManuel_methodInvalide_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"cotisationId\":\"" + UUID.randomUUID()
|
||||
+ "\",\"methodePaiement\":\"PAYPAL\"}")
|
||||
.when().post(BASE + "/declarer-manuel")
|
||||
.then().statusCode(400);
|
||||
}
|
||||
|
||||
// ── POST /initier-depot-epargne ───────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@Order(16)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierDepotEpargne_validRequest_returns201() {
|
||||
VersementGatewayResponse gateway = VersementGatewayResponse.builder()
|
||||
.waveLaunchUrl("wave://checkout/epargne")
|
||||
.statut("EN_ATTENTE")
|
||||
.montant(new BigDecimal("10000"))
|
||||
.build();
|
||||
when(versementService.initierDepotEpargneEnLigne(any())).thenReturn(gateway);
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"compteId\":\"" + UUID.randomUUID()
|
||||
+ "\",\"montant\":10000,\"numeroTelephone\":\"771234567\"}")
|
||||
.when().post(BASE + "/initier-depot-epargne")
|
||||
.then().statusCode(201)
|
||||
.body("waveLaunchUrl", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(17)
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierDepotEpargne_montantManquant_returns400() {
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body("{\"compteId\":\"" + UUID.randomUUID() + "\"}")
|
||||
.when().post(BASE + "/initier-depot-epargne")
|
||||
.then().statusCode(400);
|
||||
}
|
||||
}
|
||||
@@ -1,562 +0,0 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.communication.request.CreateConversationRequest;
|
||||
import dev.lions.unionflow.server.api.dto.communication.response.ConversationResponse;
|
||||
import dev.lions.unionflow.server.api.enums.communication.ConversationType;
|
||||
import dev.lions.unionflow.server.api.enums.communication.MessageStatus;
|
||||
import dev.lions.unionflow.server.api.enums.communication.MessageType;
|
||||
import dev.lions.unionflow.server.entity.Conversation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Message;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.ConversationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.MessageRepository;
|
||||
import dev.lions.unionflow.server.repository.OrganisationRepository;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.junit.mockito.InjectSpy;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour ConversationService
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-20
|
||||
*/
|
||||
@QuarkusTest
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class ConversationServiceTest {
|
||||
|
||||
@Inject
|
||||
ConversationService conversationService;
|
||||
|
||||
@InjectMock
|
||||
ConversationRepository conversationRepository;
|
||||
|
||||
@InjectMock
|
||||
MessageRepository messageRepository;
|
||||
|
||||
@InjectSpy
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@InjectSpy
|
||||
OrganisationRepository organisationRepository;
|
||||
|
||||
@InjectMock
|
||||
EntityManager entityManager;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private Conversation mockConversation() {
|
||||
Conversation c = new Conversation();
|
||||
c.setId(UUID.randomUUID());
|
||||
c.setName("Test Conv");
|
||||
c.setIsMuted(false);
|
||||
c.setIsPinned(false);
|
||||
c.setIsArchived(false);
|
||||
c.setParticipants(new ArrayList<>());
|
||||
return c;
|
||||
}
|
||||
|
||||
private Message mockMessage(Conversation conv) {
|
||||
Message msg = new Message();
|
||||
msg.setId(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
Membre sender = new Membre();
|
||||
sender.setId(UUID.randomUUID());
|
||||
msg.setSender(sender);
|
||||
msg.setSenderName("Test Sender");
|
||||
msg.setContent("Hello");
|
||||
msg.setType(MessageType.INDIVIDUAL);
|
||||
msg.setStatus(MessageStatus.SENT);
|
||||
msg.setIsEdited(false);
|
||||
msg.setIsDeleted(false);
|
||||
return msg;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getConversations
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("getConversations_withOrgId_callsByOrganisation")
|
||||
void getConversations_withOrgId_callsByOrganisation() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
UUID orgId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
when(conversationRepository.findByOrganisation(orgId)).thenReturn(List.of(conv));
|
||||
when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
|
||||
|
||||
List<ConversationResponse> result = conversationService.getConversations(membreId, orgId, false);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
verify(conversationRepository).findByOrganisation(orgId);
|
||||
verify(conversationRepository, never()).findByParticipant(any(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getConversations_withoutOrgId_callsByParticipant")
|
||||
void getConversations_withoutOrgId_callsByParticipant() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
when(conversationRepository.findByParticipant(membreId, false)).thenReturn(List.of(conv));
|
||||
when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
|
||||
|
||||
List<ConversationResponse> result = conversationService.getConversations(membreId, null, false);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
verify(conversationRepository).findByParticipant(membreId, false);
|
||||
verify(conversationRepository, never()).findByOrganisation(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getConversations_includesLastMessageAndUnread")
|
||||
void getConversations_includesLastMessageAndUnread() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Message lastMsg = mockMessage(conv);
|
||||
|
||||
when(conversationRepository.findByParticipant(membreId, true)).thenReturn(List.of(conv));
|
||||
when(messageRepository.findLastByConversation(conv.getId())).thenReturn(lastMsg);
|
||||
when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(3L);
|
||||
|
||||
List<ConversationResponse> result = conversationService.getConversations(membreId, null, true);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
ConversationResponse response = result.get(0);
|
||||
assertThat(response.getLastMessage()).isNotNull();
|
||||
assertThat(response.getLastMessage().getContent()).isEqualTo("Hello");
|
||||
assertThat(response.getUnreadCount()).isEqualTo(3);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getConversationById
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("getConversationById_notFound_throwsNotFound")
|
||||
void getConversationById_notFound_throwsNotFound() {
|
||||
UUID convId = UUID.randomUUID();
|
||||
UUID membreId = UUID.randomUUID();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> conversationService.getConversationById(convId, membreId))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Conversation non trouvée");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getConversationById_found_returnsResponse")
|
||||
void getConversationById_found_returnsResponse() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
|
||||
|
||||
ConversationResponse response = conversationService.getConversationById(conv.getId(), membreId);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getId()).isEqualTo(conv.getId());
|
||||
assertThat(response.getName()).isEqualTo("Test Conv");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// createConversation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("createConversation_withoutOrg_success")
|
||||
void createConversation_withoutOrg_success() {
|
||||
UUID creatorId = UUID.randomUUID();
|
||||
Membre creator = new Membre();
|
||||
creator.setId(creatorId);
|
||||
|
||||
CreateConversationRequest request = CreateConversationRequest.builder()
|
||||
.name("New Conv")
|
||||
.description("desc")
|
||||
.type(ConversationType.GROUP)
|
||||
.participantIds(new ArrayList<>())
|
||||
.organisationId(null)
|
||||
.build();
|
||||
|
||||
when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
|
||||
when(messageRepository.findLastByConversation(any())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
|
||||
|
||||
ConversationResponse response = conversationService.createConversation(request, creatorId);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getName()).isEqualTo("New Conv");
|
||||
verify(conversationRepository).persist(any(Conversation.class));
|
||||
verify(entityManager, never()).find(eq(Organisation.class), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("createConversation_withOrg_setsOrganisation")
|
||||
void createConversation_withOrg_setsOrganisation() {
|
||||
UUID creatorId = UUID.randomUUID();
|
||||
UUID orgId = UUID.randomUUID();
|
||||
Membre creator = new Membre();
|
||||
creator.setId(creatorId);
|
||||
Organisation org = new Organisation();
|
||||
org.setId(orgId);
|
||||
|
||||
CreateConversationRequest request = CreateConversationRequest.builder()
|
||||
.name("Org Conv")
|
||||
.description(null)
|
||||
.type(ConversationType.BROADCAST)
|
||||
.participantIds(new ArrayList<>())
|
||||
.organisationId(orgId)
|
||||
.build();
|
||||
|
||||
when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
|
||||
when(entityManager.find(Organisation.class, orgId)).thenReturn(org);
|
||||
when(messageRepository.findLastByConversation(any())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
|
||||
|
||||
ConversationResponse response = conversationService.createConversation(request, creatorId);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getOrganisationId()).isEqualTo(orgId);
|
||||
verify(entityManager).find(Organisation.class, orgId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("createConversation_creatorNotInList_addsCreator")
|
||||
void createConversation_creatorNotInList_addsCreator() {
|
||||
UUID creatorId = UUID.randomUUID();
|
||||
UUID participant1Id = UUID.randomUUID();
|
||||
Membre creator = new Membre();
|
||||
creator.setId(creatorId);
|
||||
Membre participant1 = new Membre();
|
||||
participant1.setId(participant1Id);
|
||||
|
||||
CreateConversationRequest request = CreateConversationRequest.builder()
|
||||
.name("Conv")
|
||||
.description(null)
|
||||
.type(ConversationType.INDIVIDUAL)
|
||||
.participantIds(new ArrayList<>(List.of(participant1Id)))
|
||||
.organisationId(null)
|
||||
.build();
|
||||
|
||||
when(entityManager.find(Membre.class, participant1Id)).thenReturn(participant1);
|
||||
when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
|
||||
when(messageRepository.findLastByConversation(any())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
|
||||
|
||||
ConversationResponse response = conversationService.createConversation(request, creatorId);
|
||||
|
||||
// Creator + participant1 = 2 participants
|
||||
assertThat(response.getParticipantIds()).hasSize(2);
|
||||
assertThat(response.getParticipantIds()).contains(creatorId, participant1Id);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("createConversation_creatorAlreadyInList_doesNotDuplicate")
|
||||
void createConversation_creatorAlreadyInList_doesNotDuplicate() {
|
||||
UUID creatorId = UUID.randomUUID();
|
||||
Membre creator = new Membre();
|
||||
creator.setId(creatorId);
|
||||
|
||||
CreateConversationRequest request = CreateConversationRequest.builder()
|
||||
.name("Conv")
|
||||
.description(null)
|
||||
.type(ConversationType.INDIVIDUAL)
|
||||
.participantIds(new ArrayList<>(List.of(creatorId)))
|
||||
.organisationId(null)
|
||||
.build();
|
||||
|
||||
// findById(creatorId) appelé 2 fois: une pour le participant, une pour le créateur
|
||||
when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
|
||||
when(messageRepository.findLastByConversation(any())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
|
||||
|
||||
ConversationResponse response = conversationService.createConversation(request, creatorId);
|
||||
|
||||
// Le créateur ne doit pas être dupliqué
|
||||
assertThat(response.getParticipantIds()).hasSize(1);
|
||||
assertThat(response.getParticipantIds()).containsExactly(creatorId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("createConversation_participantNotFound_filtersNull")
|
||||
void createConversation_participantNotFound_filtersNull() {
|
||||
UUID creatorId = UUID.randomUUID();
|
||||
UUID unknownId = UUID.randomUUID();
|
||||
Membre creator = new Membre();
|
||||
creator.setId(creatorId);
|
||||
|
||||
CreateConversationRequest request = CreateConversationRequest.builder()
|
||||
.name("Conv")
|
||||
.description(null)
|
||||
.type(ConversationType.GROUP)
|
||||
.participantIds(new ArrayList<>(List.of(unknownId)))
|
||||
.organisationId(null)
|
||||
.build();
|
||||
|
||||
when(entityManager.find(Membre.class, creatorId)).thenReturn(creator);
|
||||
when(messageRepository.findLastByConversation(any())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
|
||||
|
||||
ConversationResponse response = conversationService.createConversation(request, creatorId);
|
||||
|
||||
// unknownId est filtré, seul le créateur reste
|
||||
assertThat(response.getParticipantIds()).hasSize(1);
|
||||
assertThat(response.getParticipantIds()).containsExactly(creatorId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("createConversation_creatorNotFound_doesNotAddCreator (creator == null → L97 false)")
|
||||
void createConversation_creatorNull_doesNotAddCreator() {
|
||||
UUID creatorId = UUID.randomUUID();
|
||||
|
||||
CreateConversationRequest request = CreateConversationRequest.builder()
|
||||
.name("No Creator Conv")
|
||||
.description(null)
|
||||
.type(ConversationType.GROUP)
|
||||
.participantIds(new ArrayList<>())
|
||||
.organisationId(null)
|
||||
.build();
|
||||
|
||||
// creator == null → condition L97: creator != null = false → pas d'ajout du créateur
|
||||
when(entityManager.find(Membre.class, creatorId)).thenReturn(null);
|
||||
when(messageRepository.findLastByConversation(any())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(any(), eq(creatorId))).thenReturn(0L);
|
||||
|
||||
ConversationResponse response = conversationService.createConversation(request, creatorId);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
// Aucun participant car creator introuvable et liste vide
|
||||
assertThat(response.getParticipantIds()).isEmpty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// archiveConversation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("archiveConversation_notFound_throwsNotFound")
|
||||
void archiveConversation_notFound_throwsNotFound() {
|
||||
UUID convId = UUID.randomUUID();
|
||||
UUID membreId = UUID.randomUUID();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> conversationService.archiveConversation(convId, membreId, true))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Conversation non trouvée");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("archiveConversation_archive_setsTrue")
|
||||
void archiveConversation_archive_setsTrue() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
conv.setIsArchived(false);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
|
||||
conversationService.archiveConversation(conv.getId(), membreId, true);
|
||||
|
||||
assertThat(conv.getIsArchived()).isTrue();
|
||||
verify(conversationRepository).persist(conv);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("archiveConversation_unarchive_setsFalse")
|
||||
void archiveConversation_unarchive_setsFalse() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
conv.setIsArchived(true);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
|
||||
conversationService.archiveConversation(conv.getId(), membreId, false);
|
||||
|
||||
assertThat(conv.getIsArchived()).isFalse();
|
||||
verify(conversationRepository).persist(conv);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// markAsRead
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("markAsRead_notFound_throwsNotFound")
|
||||
void markAsRead_notFound_throwsNotFound() {
|
||||
UUID convId = UUID.randomUUID();
|
||||
UUID membreId = UUID.randomUUID();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> conversationService.markAsRead(convId, membreId))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Conversation non trouvée");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("markAsRead_success_callsRepo")
|
||||
void markAsRead_success_callsRepo() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
when(messageRepository.markAllAsReadByConversationAndMember(conv.getId(), membreId)).thenReturn(5);
|
||||
|
||||
conversationService.markAsRead(conv.getId(), membreId);
|
||||
|
||||
verify(messageRepository).markAllAsReadByConversationAndMember(conv.getId(), membreId);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// toggleMute
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("toggleMute_notFound_throwsNotFound")
|
||||
void toggleMute_notFound_throwsNotFound() {
|
||||
UUID convId = UUID.randomUUID();
|
||||
UUID membreId = UUID.randomUUID();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> conversationService.toggleMute(convId, membreId))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Conversation non trouvée");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toggleMute_false_setsTrue")
|
||||
void toggleMute_false_setsTrue() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
conv.setIsMuted(false);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
|
||||
conversationService.toggleMute(conv.getId(), membreId);
|
||||
|
||||
assertThat(conv.getIsMuted()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toggleMute_true_setsFalse")
|
||||
void toggleMute_true_setsFalse() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
conv.setIsMuted(true);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
|
||||
conversationService.toggleMute(conv.getId(), membreId);
|
||||
|
||||
assertThat(conv.getIsMuted()).isFalse();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// togglePin
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("togglePin_notFound_throwsNotFound")
|
||||
void togglePin_notFound_throwsNotFound() {
|
||||
UUID convId = UUID.randomUUID();
|
||||
UUID membreId = UUID.randomUUID();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> conversationService.togglePin(convId, membreId))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Conversation non trouvée");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("togglePin_false_setsTrue")
|
||||
void togglePin_false_setsTrue() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
conv.setIsPinned(false);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
|
||||
conversationService.togglePin(conv.getId(), membreId);
|
||||
|
||||
assertThat(conv.getIsPinned()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("togglePin_true_setsFalse")
|
||||
void togglePin_true_setsFalse() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
conv.setIsPinned(true);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
|
||||
conversationService.togglePin(conv.getId(), membreId);
|
||||
|
||||
assertThat(conv.getIsPinned()).isFalse();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// convertToResponse (via getConversationById)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_withLastMessage_includesMessage")
|
||||
void convertToResponse_withLastMessage_includesMessage() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Message lastMsg = mockMessage(conv);
|
||||
lastMsg.setContent("Dernier message");
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
when(messageRepository.findLastByConversation(conv.getId())).thenReturn(lastMsg);
|
||||
when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
|
||||
|
||||
ConversationResponse response = conversationService.getConversationById(conv.getId(), membreId);
|
||||
|
||||
assertThat(response.getLastMessage()).isNotNull();
|
||||
assertThat(response.getLastMessage().getContent()).isEqualTo("Dernier message");
|
||||
assertThat(response.getLastMessage().getId()).isEqualTo(lastMsg.getId());
|
||||
assertThat(response.getLastMessage().getSenderId()).isEqualTo(lastMsg.getSender().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_noLastMessage_nullLastMessage")
|
||||
void convertToResponse_noLastMessage_nullLastMessage() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
when(messageRepository.findLastByConversation(conv.getId())).thenReturn(null);
|
||||
when(messageRepository.countUnreadByConversationAndMember(conv.getId(), membreId)).thenReturn(0L);
|
||||
|
||||
ConversationResponse response = conversationService.getConversationById(conv.getId(), membreId);
|
||||
|
||||
assertThat(response.getLastMessage()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -674,14 +674,19 @@ class MembreServiceTest {
|
||||
doReturn(100L).when(membreRepository).count();
|
||||
doReturn(80L).when(membreRepository).countActifs();
|
||||
doReturn(10L).when(membreRepository).countNouveauxMembres(any());
|
||||
doReturn(5L).when(organisationService).rechercherOrganisationsCount("");
|
||||
|
||||
Map<String, Object> stats = membreService.obtenirStatistiquesAvancees();
|
||||
|
||||
assertThat(stats).containsKey("totalMembres");
|
||||
assertThat(stats).containsKey("total"); // alias mobile
|
||||
assertThat(stats).containsKey("totalOrganisations");
|
||||
assertThat(stats).containsKey("membresActifs");
|
||||
assertThat(stats).containsKey("membresInactifs");
|
||||
assertThat(stats).containsKey("tauxActivite");
|
||||
assertThat(stats.get("totalMembres")).isEqualTo(100L);
|
||||
assertThat(stats.get("total")).isEqualTo(100L);
|
||||
assertThat(stats.get("totalOrganisations")).isEqualTo(5L);
|
||||
assertThat(stats.get("membresActifs")).isEqualTo(80L);
|
||||
assertThat(stats.get("membresInactifs")).isEqualTo(20L);
|
||||
assertThat((Double) stats.get("tauxActivite")).isEqualTo(80.0);
|
||||
@@ -693,10 +698,12 @@ class MembreServiceTest {
|
||||
doReturn(0L).when(membreRepository).count();
|
||||
doReturn(0L).when(membreRepository).countActifs();
|
||||
doReturn(0L).when(membreRepository).countNouveauxMembres(any());
|
||||
doReturn(0L).when(organisationService).rechercherOrganisationsCount("");
|
||||
|
||||
Map<String, Object> stats = membreService.obtenirStatistiquesAvancees();
|
||||
|
||||
assertThat((Double) stats.get("tauxActivite")).isEqualTo(0.0);
|
||||
assertThat(stats.get("totalOrganisations")).isEqualTo(0L);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -1,621 +0,0 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.communication.request.SendMessageRequest;
|
||||
import dev.lions.unionflow.server.api.dto.communication.response.MessageResponse;
|
||||
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 dev.lions.unionflow.server.entity.Conversation;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Message;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.ConversationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.MessageRepository;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.junit.mockito.InjectSpy;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour MessageService
|
||||
*
|
||||
* @author UnionFlow Team
|
||||
* @version 1.0
|
||||
* @since 2026-03-20
|
||||
*/
|
||||
@QuarkusTest
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class MessageServiceTest {
|
||||
|
||||
@Inject
|
||||
MessageService messageService;
|
||||
|
||||
@InjectSpy
|
||||
MessageRepository messageRepository;
|
||||
|
||||
@InjectMock
|
||||
ConversationRepository conversationRepository;
|
||||
|
||||
@InjectSpy
|
||||
MembreRepository membreRepository;
|
||||
|
||||
@InjectMock
|
||||
EntityManager entityManager;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private Conversation mockConversation() {
|
||||
Conversation c = new Conversation();
|
||||
c.setId(UUID.randomUUID());
|
||||
c.setParticipants(new ArrayList<>());
|
||||
return c;
|
||||
}
|
||||
|
||||
private Membre mockMembre(UUID id) {
|
||||
Membre m = new Membre();
|
||||
m.setId(id);
|
||||
m.setPrenom("Jean");
|
||||
m.setNom("Dupont");
|
||||
return m;
|
||||
}
|
||||
|
||||
private Message mockMessage(UUID senderId) {
|
||||
Message msg = new Message();
|
||||
msg.setId(UUID.randomUUID());
|
||||
Conversation conv = mockConversation();
|
||||
msg.setConversation(conv);
|
||||
Membre sender = mockMembre(senderId);
|
||||
msg.setSender(sender);
|
||||
msg.setSenderName("Jean Dupont");
|
||||
msg.setContent("Hello");
|
||||
msg.setType(MessageType.INDIVIDUAL);
|
||||
msg.setStatus(MessageStatus.SENT);
|
||||
msg.setPriority(MessagePriority.NORMAL);
|
||||
msg.setIsEdited(false);
|
||||
msg.setIsDeleted(false);
|
||||
return msg;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getMessages
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("getMessages_conversationNotFound_throwsNotFound")
|
||||
void getMessages_conversationNotFound_throwsNotFound() {
|
||||
UUID convId = UUID.randomUUID();
|
||||
UUID membreId = UUID.randomUUID();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(convId, membreId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> messageService.getMessages(convId, membreId, 20))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Conversation non trouvée");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getMessages_found_returnsList")
|
||||
void getMessages_found_returnsList() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getContent()).isEqualTo("Hello");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// sendMessage
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("sendMessage_conversationNotFound_throwsNotFound")
|
||||
void sendMessage_conversationNotFound_throwsNotFound() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
UUID convId = UUID.randomUUID();
|
||||
|
||||
SendMessageRequest request = SendMessageRequest.builder()
|
||||
.conversationId(convId)
|
||||
.content("test")
|
||||
.build();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(convId, senderId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> messageService.sendMessage(request, senderId))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Conversation non trouvée");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sendMessage_senderNotFound_throwsNotFound")
|
||||
void sendMessage_senderNotFound_throwsNotFound() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
SendMessageRequest request = SendMessageRequest.builder()
|
||||
.conversationId(conv.getId())
|
||||
.content("test")
|
||||
.build();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
|
||||
// entityManager.find returns null by default → findById(senderId) returns null → service throws
|
||||
|
||||
assertThatThrownBy(() -> messageService.sendMessage(request, senderId))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Expéditeur non trouvé");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sendMessage_withDefaultTypeAndPriority_success")
|
||||
void sendMessage_withDefaultTypeAndPriority_success() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Membre sender = mockMembre(senderId);
|
||||
|
||||
SendMessageRequest request = SendMessageRequest.builder()
|
||||
.conversationId(conv.getId())
|
||||
.content("Bonjour")
|
||||
.type(null)
|
||||
.priority(null)
|
||||
.recipientIds(null)
|
||||
.recipientRoles(null)
|
||||
.attachments(null)
|
||||
.build();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
|
||||
when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
|
||||
|
||||
MessageResponse response = messageService.sendMessage(request, senderId);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getContent()).isEqualTo("Bonjour");
|
||||
assertThat(response.getType()).isEqualTo(MessageType.INDIVIDUAL);
|
||||
assertThat(response.getPriority()).isEqualTo(MessagePriority.NORMAL);
|
||||
assertThat(response.getSenderName()).isEqualTo("Jean Dupont");
|
||||
verify(messageRepository).persist(any(Message.class));
|
||||
verify(conversationRepository).persist(conv);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sendMessage_withExplicitType_usesType")
|
||||
void sendMessage_withExplicitType_usesType() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Membre sender = mockMembre(senderId);
|
||||
|
||||
SendMessageRequest request = SendMessageRequest.builder()
|
||||
.conversationId(conv.getId())
|
||||
.content("Broadcast!")
|
||||
.type(MessageType.BROADCAST)
|
||||
.priority(MessagePriority.HIGH)
|
||||
.recipientIds(null)
|
||||
.recipientRoles(null)
|
||||
.attachments(null)
|
||||
.build();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
|
||||
when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
|
||||
|
||||
MessageResponse response = messageService.sendMessage(request, senderId);
|
||||
|
||||
assertThat(response.getType()).isEqualTo(MessageType.BROADCAST);
|
||||
assertThat(response.getPriority()).isEqualTo(MessagePriority.HIGH);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sendMessage_withRecipientIds_setsCSV")
|
||||
void sendMessage_withRecipientIds_setsCSV() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
UUID recipient1 = UUID.randomUUID();
|
||||
UUID recipient2 = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Membre sender = mockMembre(senderId);
|
||||
|
||||
SendMessageRequest request = SendMessageRequest.builder()
|
||||
.conversationId(conv.getId())
|
||||
.content("Targeted")
|
||||
.type(MessageType.TARGETED)
|
||||
.priority(null)
|
||||
.recipientIds(List.of(recipient1, recipient2))
|
||||
.recipientRoles(null)
|
||||
.attachments(null)
|
||||
.build();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
|
||||
when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
|
||||
|
||||
MessageResponse response = messageService.sendMessage(request, senderId);
|
||||
|
||||
assertThat(response.getRecipientIds()).isNotNull();
|
||||
assertThat(response.getRecipientIds()).containsExactlyInAnyOrder(recipient1, recipient2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sendMessage_withRecipientRoles_setsCSV")
|
||||
void sendMessage_withRecipientRoles_setsCSV() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Membre sender = mockMembre(senderId);
|
||||
|
||||
SendMessageRequest request = SendMessageRequest.builder()
|
||||
.conversationId(conv.getId())
|
||||
.content("Role msg")
|
||||
.type(null)
|
||||
.priority(null)
|
||||
.recipientIds(null)
|
||||
.recipientRoles(List.of("ADMIN", "TRESORIER"))
|
||||
.attachments(null)
|
||||
.build();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
|
||||
when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
|
||||
|
||||
MessageResponse response = messageService.sendMessage(request, senderId);
|
||||
|
||||
assertThat(response.getRecipientRoles()).isNotNull();
|
||||
assertThat(response.getRecipientRoles()).containsExactly("ADMIN", "TRESORIER");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sendMessage_withAttachments_setsCSV")
|
||||
void sendMessage_withAttachments_setsCSV() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Membre sender = mockMembre(senderId);
|
||||
|
||||
SendMessageRequest request = SendMessageRequest.builder()
|
||||
.conversationId(conv.getId())
|
||||
.content("Msg avec PJ")
|
||||
.type(null)
|
||||
.priority(null)
|
||||
.recipientIds(null)
|
||||
.recipientRoles(null)
|
||||
.attachments(List.of("https://cdn.example.com/doc1.pdf", "https://cdn.example.com/img1.png"))
|
||||
.build();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
|
||||
when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
|
||||
|
||||
MessageResponse response = messageService.sendMessage(request, senderId);
|
||||
|
||||
assertThat(response.getAttachments()).isNotNull();
|
||||
assertThat(response.getAttachments()).containsExactly(
|
||||
"https://cdn.example.com/doc1.pdf",
|
||||
"https://cdn.example.com/img1.png"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("sendMessage_noRecipientsNoRolesNoAttachments_noCSV")
|
||||
void sendMessage_noRecipientsNoRolesNoAttachments_noCSV() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Membre sender = mockMembre(senderId);
|
||||
|
||||
SendMessageRequest request = SendMessageRequest.builder()
|
||||
.conversationId(conv.getId())
|
||||
.content("Simple")
|
||||
.type(null)
|
||||
.priority(null)
|
||||
.recipientIds(new ArrayList<>())
|
||||
.recipientRoles(new ArrayList<>())
|
||||
.attachments(new ArrayList<>())
|
||||
.build();
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), senderId)).thenReturn(Optional.of(conv));
|
||||
when(entityManager.find(Membre.class, senderId)).thenReturn(sender);
|
||||
|
||||
MessageResponse response = messageService.sendMessage(request, senderId);
|
||||
|
||||
assertThat(response.getRecipientIds()).isNull();
|
||||
assertThat(response.getRecipientRoles()).isNull();
|
||||
assertThat(response.getAttachments()).isNull();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// editMessage
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("editMessage_notFound_throwsNotFound")
|
||||
void editMessage_notFound_throwsNotFound() {
|
||||
UUID messageId = UUID.randomUUID();
|
||||
UUID senderId = UUID.randomUUID();
|
||||
|
||||
// entityManager.find returns null by default → findById(messageId) returns null → service throws
|
||||
|
||||
assertThatThrownBy(() -> messageService.editMessage(messageId, senderId, "nouveau contenu"))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Message non trouvé");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("editMessage_wrongSender_throwsIllegalState")
|
||||
void editMessage_wrongSender_throwsIllegalState() {
|
||||
UUID realSenderId = UUID.randomUUID();
|
||||
UUID wrongSenderId = UUID.randomUUID();
|
||||
Message msg = mockMessage(realSenderId);
|
||||
|
||||
when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);
|
||||
|
||||
assertThatThrownBy(() -> messageService.editMessage(msg.getId(), wrongSenderId, "contenu modifié"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("propres messages");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("editMessage_success_updatesContent")
|
||||
void editMessage_success_updatesContent() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
Message msg = mockMessage(senderId);
|
||||
msg.setIsEdited(false);
|
||||
|
||||
when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);
|
||||
|
||||
MessageResponse response = messageService.editMessage(msg.getId(), senderId, "Contenu édité");
|
||||
|
||||
assertThat(msg.getContent()).isEqualTo("Contenu édité");
|
||||
assertThat(msg.getIsEdited()).isTrue();
|
||||
assertThat(msg.getEditedAt()).isNotNull();
|
||||
verify(messageRepository).persist(msg);
|
||||
assertThat(response.getContent()).isEqualTo("Contenu édité");
|
||||
assertThat(response.isEdited()).isTrue();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// deleteMessage
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("deleteMessage_notFound_throwsNotFound")
|
||||
void deleteMessage_notFound_throwsNotFound() {
|
||||
UUID messageId = UUID.randomUUID();
|
||||
UUID senderId = UUID.randomUUID();
|
||||
|
||||
// entityManager.find returns null by default → findById(messageId) returns null → service throws
|
||||
|
||||
assertThatThrownBy(() -> messageService.deleteMessage(messageId, senderId))
|
||||
.isInstanceOf(NotFoundException.class)
|
||||
.hasMessageContaining("Message non trouvé");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("deleteMessage_wrongSender_throwsIllegalState")
|
||||
void deleteMessage_wrongSender_throwsIllegalState() {
|
||||
UUID realSenderId = UUID.randomUUID();
|
||||
UUID wrongSenderId = UUID.randomUUID();
|
||||
Message msg = mockMessage(realSenderId);
|
||||
|
||||
when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);
|
||||
|
||||
assertThatThrownBy(() -> messageService.deleteMessage(msg.getId(), wrongSenderId))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("propres messages");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("deleteMessage_success_softDeletes")
|
||||
void deleteMessage_success_softDeletes() {
|
||||
UUID senderId = UUID.randomUUID();
|
||||
Message msg = mockMessage(senderId);
|
||||
msg.setIsDeleted(false);
|
||||
msg.setContent("Contenu original");
|
||||
|
||||
when(entityManager.find(Message.class, msg.getId())).thenReturn(msg);
|
||||
|
||||
messageService.deleteMessage(msg.getId(), senderId);
|
||||
|
||||
assertThat(msg.getIsDeleted()).isTrue();
|
||||
assertThat(msg.getContent()).isEqualTo("[Message supprimé]");
|
||||
verify(messageRepository).persist(msg);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// convertToResponse (via getMessages)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_withRecipientIds_parsesCsv")
|
||||
void convertToResponse_withRecipientIds_parsesCsv() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
UUID r1 = UUID.randomUUID();
|
||||
UUID r2 = UUID.randomUUID();
|
||||
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
msg.setRecipientIds(r1 + "," + r2);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getRecipientIds()).containsExactlyInAnyOrder(r1, r2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_withRecipientRoles_parsesCsv")
|
||||
void convertToResponse_withRecipientRoles_parsesCsv() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
msg.setRecipientRoles("ADMIN,SECRETAIRE");
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
assertThat(result.get(0).getRecipientRoles()).containsExactly("ADMIN", "SECRETAIRE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_withAttachments_parsesCsv")
|
||||
void convertToResponse_withAttachments_parsesCsv() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
msg.setAttachments("https://cdn.example.com/a.pdf,https://cdn.example.com/b.png");
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
assertThat(result.get(0).getAttachments()).containsExactly(
|
||||
"https://cdn.example.com/a.pdf",
|
||||
"https://cdn.example.com/b.png"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_noRecipients_nullFields")
|
||||
void convertToResponse_noRecipients_nullFields() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
// recipientIds, recipientRoles et attachments sont null par défaut
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
MessageResponse response = result.get(0);
|
||||
assertThat(response.getRecipientIds()).isNull();
|
||||
assertThat(response.getRecipientRoles()).isNull();
|
||||
assertThat(response.getAttachments()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_withOrganisation_setsOrgId")
|
||||
void convertToResponse_withOrganisation_setsOrgId() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
UUID orgId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
Organisation org = new Organisation();
|
||||
org.setId(orgId);
|
||||
conv.setOrganisation(org);
|
||||
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
msg.setOrganisation(org);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
assertThat(result.get(0).getOrganisationId()).isEqualTo(orgId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_noOrganisation_nullOrgId")
|
||||
void convertToResponse_noOrganisation_nullOrgId() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
// organisation est null
|
||||
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
msg.setOrganisation(null);
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
assertThat(result.get(0).getOrganisationId()).isNull();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// convertToResponse — branches isEmpty() (non-null mais vide)
|
||||
// L163: recipientIds != null && !isEmpty() → false (empty string → isEmpty = true)
|
||||
// L172: recipientRoles != null && !isEmpty() → false
|
||||
// L178: attachments != null && !isEmpty() → false
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_recipientIdsEmptyString_returnsNullRecipientIds (L163 false)")
|
||||
void convertToResponse_recipientIdsEmptyString_returnsNullRecipientIds() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
// non-null mais vide → L163: isEmpty() = true → condition false → recipientIds = null
|
||||
msg.setRecipientIds("");
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
assertThat(result.get(0).getRecipientIds()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_recipientRolesEmptyString_returnsNullRoles (L172 false)")
|
||||
void convertToResponse_recipientRolesEmptyString_returnsNullRoles() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
msg.setRecipientRoles(""); // non-null mais vide → L172 false → roles = null
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
assertThat(result.get(0).getRecipientRoles()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertToResponse_attachmentsEmptyString_returnsNullAttachments (L178 false)")
|
||||
void convertToResponse_attachmentsEmptyString_returnsNullAttachments() {
|
||||
UUID membreId = UUID.randomUUID();
|
||||
Conversation conv = mockConversation();
|
||||
|
||||
Message msg = mockMessage(UUID.randomUUID());
|
||||
msg.setConversation(conv);
|
||||
msg.setAttachments(""); // non-null mais vide → L178 false → attachments = null
|
||||
|
||||
when(conversationRepository.findByIdAndParticipant(conv.getId(), membreId)).thenReturn(Optional.of(conv));
|
||||
doReturn(List.of(msg)).when(messageRepository).findByConversation(conv.getId(), 20);
|
||||
|
||||
List<MessageResponse> result = messageService.getMessages(conv.getId(), membreId, 20);
|
||||
|
||||
assertThat(result.get(0).getAttachments()).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.BloquerMembreRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationDirecteRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.DemarrerConversationRoleRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.EnvoyerMessageRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.request.MettreAJourPolitiqueRequest;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ContactPolicyResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.ConversationSummaryResponse;
|
||||
import dev.lions.unionflow.server.api.dto.messagerie.response.MessageResponse;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.StatutConversation;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeContenu;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypeConversation;
|
||||
import dev.lions.unionflow.server.api.enums.messagerie.TypePolitiqueCommunication;
|
||||
import dev.lions.unionflow.server.entity.ContactPolicy;
|
||||
import dev.lions.unionflow.server.entity.Conversation;
|
||||
import dev.lions.unionflow.server.entity.ConversationParticipant;
|
||||
import dev.lions.unionflow.server.entity.MemberBlock;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Message;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.repository.ContactPolicyRepository;
|
||||
import dev.lions.unionflow.server.repository.ConversationParticipantRepository;
|
||||
import dev.lions.unionflow.server.repository.ConversationRepository;
|
||||
import dev.lions.unionflow.server.repository.MemberBlockRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.MessageRepository;
|
||||
import dev.lions.unionflow.server.messaging.KafkaEventProducer;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.ForbiddenException;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests unitaires du MessagingService avec mocks.
|
||||
*/
|
||||
@QuarkusTest
|
||||
@DisplayName("MessagingService")
|
||||
class MessagingServiceTest {
|
||||
|
||||
@Inject
|
||||
MessagingService messagingService;
|
||||
|
||||
@InjectMock ConversationRepository conversationRepository;
|
||||
@InjectMock ConversationParticipantRepository participantRepository;
|
||||
@InjectMock MessageRepository messageRepository;
|
||||
@InjectMock ContactPolicyRepository contactPolicyRepository;
|
||||
@InjectMock MemberBlockRepository memberBlockRepository;
|
||||
@InjectMock MembreRepository membreRepository;
|
||||
@InjectMock MembreOrganisationRepository membreOrganisationRepository;
|
||||
@InjectMock KafkaEventProducer kafkaEventProducer;
|
||||
@InjectMock SecurityIdentity securityIdentity;
|
||||
|
||||
private static final UUID MEMBRE_ID = UUID.fromString("00000000-0000-0000-0000-000000000001");
|
||||
private static final UUID DESTINATAIRE_ID = UUID.fromString("00000000-0000-0000-0000-000000000002");
|
||||
private static final UUID ORG_ID = UUID.fromString("00000000-0000-0000-0000-000000000003");
|
||||
private static final UUID CONV_ID = UUID.fromString("00000000-0000-0000-0000-000000000004");
|
||||
private static final UUID MESSAGE_ID = UUID.fromString("00000000-0000-0000-0000-000000000005");
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Mock du membre connecté
|
||||
io.quarkus.security.runtime.QuarkusPrincipal principal =
|
||||
new io.quarkus.security.runtime.QuarkusPrincipal("membre@test.com");
|
||||
when(securityIdentity.getPrincipal()).thenReturn(principal);
|
||||
|
||||
Membre moi = newMembre(MEMBRE_ID, "membre@test.com", "Alpha", "Diallo");
|
||||
when(membreRepository.find("email", "membre@test.com"))
|
||||
.thenReturn(io.quarkus.hibernate.orm.panache.PanacheQuery.class.cast(
|
||||
mockSingleResultQuery(moi)));
|
||||
}
|
||||
|
||||
// ── getMesConversations ───────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("getMesConversations — retourne liste vide")
|
||||
void getMesConversations_returnsEmpty() {
|
||||
when(conversationRepository.findByMembreId(MEMBRE_ID))
|
||||
.thenReturn(Collections.emptyList());
|
||||
|
||||
List<ConversationSummaryResponse> result = messagingService.getMesConversations();
|
||||
|
||||
assertThat(result).isNotNull().isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getMesConversations — retourne la liste des conversations")
|
||||
void getMesConversations_returnsList() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
when(conversationRepository.findByMembreId(MEMBRE_ID)).thenReturn(List.of(conv));
|
||||
when(messageRepository.findDernierMessage(CONV_ID)).thenReturn(Optional.empty());
|
||||
when(messageRepository.countNonLus(CONV_ID, MEMBRE_ID)).thenReturn(0L);
|
||||
when(participantRepository.findByConversation(CONV_ID)).thenReturn(Collections.emptyList());
|
||||
|
||||
List<ConversationSummaryResponse> result = messagingService.getMesConversations();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getTypeConversation()).isEqualTo("DIRECTE");
|
||||
}
|
||||
|
||||
// ── getConversation ───────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("getConversation — notFound → NotFoundException")
|
||||
void getConversation_notFound() {
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> messagingService.getConversation(CONV_ID))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getConversation — non participant → ForbiddenException")
|
||||
void getConversation_nonParticipant() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
|
||||
when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(false);
|
||||
|
||||
assertThatThrownBy(() -> messagingService.getConversation(CONV_ID))
|
||||
.isInstanceOf(ForbiddenException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getConversation — success → retourne ConversationResponse")
|
||||
void getConversation_success() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
|
||||
when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
|
||||
when(participantRepository.findByConversation(CONV_ID)).thenReturn(Collections.emptyList());
|
||||
when(messageRepository.findByConversationPagine(eq(CONV_ID), eq(0), anyInt()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
when(messageRepository.countNonLus(CONV_ID, MEMBRE_ID)).thenReturn(2L);
|
||||
|
||||
ConversationResponse result = messagingService.getConversation(CONV_ID);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getId()).isEqualTo(CONV_ID);
|
||||
assertThat(result.getNonLus()).isEqualTo(2L);
|
||||
}
|
||||
|
||||
// ── archiverConversation ──────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("archiverConversation — success → statut ARCHIVEE")
|
||||
void archiverConversation_success() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
|
||||
when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
|
||||
when(participantRepository.findByConversation(CONV_ID)).thenReturn(Collections.emptyList());
|
||||
when(messageRepository.findByConversationPagine(eq(CONV_ID), eq(0), anyInt()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
when(messageRepository.countNonLus(CONV_ID, MEMBRE_ID)).thenReturn(0L);
|
||||
|
||||
ConversationResponse result = messagingService.archiverConversation(CONV_ID);
|
||||
|
||||
assertThat(result.getStatut()).isEqualTo("ARCHIVEE");
|
||||
}
|
||||
|
||||
// ── envoyerMessage ────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("envoyerMessage — conversation non trouvée → NotFoundException")
|
||||
void envoyerMessage_notFound() {
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.empty());
|
||||
|
||||
EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
|
||||
.typeMessage("TEXTE").contenu("Bonjour").build();
|
||||
|
||||
assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("envoyerMessage — non participant → ForbiddenException")
|
||||
void envoyerMessage_nonParticipant() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
|
||||
when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(false);
|
||||
|
||||
EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
|
||||
.typeMessage("TEXTE").contenu("Bonjour").build();
|
||||
|
||||
assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
|
||||
.isInstanceOf(ForbiddenException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("envoyerMessage — conversation archivée → BadRequestException")
|
||||
void envoyerMessage_archived() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
conv.setStatut(StatutConversation.ARCHIVEE);
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
|
||||
when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
|
||||
|
||||
EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
|
||||
.typeMessage("TEXTE").contenu("Bonjour").build();
|
||||
|
||||
assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
|
||||
.isInstanceOf(BadRequestException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("envoyerMessage — TEXTE sans contenu → BadRequestException")
|
||||
void envoyerMessage_texteSansContenu() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
|
||||
when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
|
||||
|
||||
EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
|
||||
.typeMessage("TEXTE").contenu("").build();
|
||||
|
||||
assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
|
||||
.isInstanceOf(BadRequestException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("envoyerMessage — VOCAL sans urlFichier → BadRequestException")
|
||||
void envoyerMessage_vocalSansUrl() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
|
||||
when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
|
||||
|
||||
EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
|
||||
.typeMessage("VOCAL").dureeAudio(30).build();
|
||||
|
||||
assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
|
||||
.isInstanceOf(BadRequestException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("envoyerMessage — VOCAL sans dureeAudio → BadRequestException")
|
||||
void envoyerMessage_vocalSansDuree() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
|
||||
when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
|
||||
|
||||
EnvoyerMessageRequest req = EnvoyerMessageRequest.builder()
|
||||
.typeMessage("VOCAL").urlFichier("https://example.com/audio.opus").build();
|
||||
|
||||
assertThatThrownBy(() -> messagingService.envoyerMessage(CONV_ID, req))
|
||||
.isInstanceOf(BadRequestException.class);
|
||||
}
|
||||
|
||||
// ── getMessages ───────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("getMessages — success → retourne liste de messages")
|
||||
void getMessages_success() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
Message msg = buildMessage(conv);
|
||||
when(conversationRepository.findConversationById(CONV_ID)).thenReturn(Optional.of(conv));
|
||||
when(participantRepository.estParticipant(CONV_ID, MEMBRE_ID)).thenReturn(true);
|
||||
when(messageRepository.findByConversationPagine(eq(CONV_ID), eq(0), anyInt()))
|
||||
.thenReturn(List.of(msg));
|
||||
|
||||
List<MessageResponse> result = messagingService.getMessages(CONV_ID, 0);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getTypeMessage()).isEqualTo("TEXTE");
|
||||
}
|
||||
|
||||
// ── marquerConversationLue ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("marquerConversationLue — participant trouvé → luJusqua mis à jour")
|
||||
void marquerConversationLue_success() {
|
||||
ConversationParticipant participant = new ConversationParticipant();
|
||||
participant.setMembre(newMembre(MEMBRE_ID, "membre@test.com", "Alpha", "Diallo"));
|
||||
when(participantRepository.findParticipant(CONV_ID, MEMBRE_ID))
|
||||
.thenReturn(Optional.of(participant));
|
||||
|
||||
messagingService.marquerConversationLue(CONV_ID);
|
||||
|
||||
assertThat(participant.getLuJusqua()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("marquerConversationLue — participant absent → no-op")
|
||||
void marquerConversationLue_noParticipant() {
|
||||
when(participantRepository.findParticipant(CONV_ID, MEMBRE_ID))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
// ne lève pas d'exception
|
||||
messagingService.marquerConversationLue(CONV_ID);
|
||||
}
|
||||
|
||||
// ── supprimerMessage ──────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("supprimerMessage — message non trouvé → NotFoundException")
|
||||
void supprimerMessage_notFound() {
|
||||
when(messageRepository.findMessageById(MESSAGE_ID)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> messagingService.supprimerMessage(CONV_ID, MESSAGE_ID))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("supprimerMessage — message dans autre conversation → NotFoundException")
|
||||
void supprimerMessage_wrongConversation() {
|
||||
Conversation autreConv = buildConversation(TypeConversation.DIRECTE);
|
||||
autreConv.setId(UUID.randomUUID()); // ID différent de CONV_ID
|
||||
Message msg = buildMessage(autreConv);
|
||||
when(messageRepository.findMessageById(MESSAGE_ID)).thenReturn(Optional.of(msg));
|
||||
|
||||
assertThatThrownBy(() -> messagingService.supprimerMessage(CONV_ID, MESSAGE_ID))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("supprimerMessage — pas l'auteur → ForbiddenException")
|
||||
void supprimerMessage_notAuthor() {
|
||||
Conversation conv = buildConversation(TypeConversation.DIRECTE);
|
||||
Membre autreExpedireur = newMembre(DESTINATAIRE_ID, "other@test.com", "Beta", "Koné");
|
||||
Message msg = new Message();
|
||||
msg.setId(MESSAGE_ID);
|
||||
msg.setConversation(conv);
|
||||
msg.setExpediteur(autreExpedireur);
|
||||
msg.setTypeMessage(TypeContenu.TEXTE);
|
||||
msg.setContenu("Message d'un autre");
|
||||
when(messageRepository.findMessageById(MESSAGE_ID)).thenReturn(Optional.of(msg));
|
||||
|
||||
assertThatThrownBy(() -> messagingService.supprimerMessage(CONV_ID, MESSAGE_ID))
|
||||
.isInstanceOf(ForbiddenException.class);
|
||||
}
|
||||
|
||||
// ── bloquerMembre ─────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("bloquerMembre — soi-même → BadRequestException")
|
||||
void bloquerMembre_soiMeme() {
|
||||
BloquerMembreRequest req = BloquerMembreRequest.builder()
|
||||
.membreABloquerId(MEMBRE_ID)
|
||||
.organisationId(ORG_ID)
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> messagingService.bloquerMembre(req))
|
||||
.isInstanceOf(BadRequestException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("bloquerMembre — membre introuvable → NotFoundException")
|
||||
void bloquerMembre_destinataireInconnu() {
|
||||
when(membreRepository.findById(DESTINATAIRE_ID)).thenReturn(null);
|
||||
|
||||
BloquerMembreRequest req = BloquerMembreRequest.builder()
|
||||
.membreABloquerId(DESTINATAIRE_ID)
|
||||
.organisationId(ORG_ID)
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> messagingService.bloquerMembre(req))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("bloquerMembre — déjà bloqué → BadRequestException")
|
||||
void bloquerMembre_dejaBloque() {
|
||||
Membre aBloquer = newMembre(DESTINATAIRE_ID, "dest@test.com", "Beta", "Koné");
|
||||
when(membreRepository.findById(DESTINATAIRE_ID)).thenReturn(aBloquer);
|
||||
when(memberBlockRepository.estBloque(MEMBRE_ID, DESTINATAIRE_ID, ORG_ID)).thenReturn(true);
|
||||
|
||||
BloquerMembreRequest req = BloquerMembreRequest.builder()
|
||||
.membreABloquerId(DESTINATAIRE_ID)
|
||||
.organisationId(ORG_ID)
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> messagingService.bloquerMembre(req))
|
||||
.isInstanceOf(BadRequestException.class);
|
||||
}
|
||||
|
||||
// ── debloquerMembre ───────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("debloquerMembre — blocage non trouvé → NotFoundException")
|
||||
void debloquerMembre_notFound() {
|
||||
when(memberBlockRepository.findBlocage(MEMBRE_ID, DESTINATAIRE_ID, ORG_ID))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> messagingService.debloquerMembre(DESTINATAIRE_ID, ORG_ID))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
// ── getPolitique ──────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("getPolitique — politique existante → retourne la politique")
|
||||
void getPolitique_existing() {
|
||||
Organisation org = newOrg();
|
||||
ContactPolicy policy = ContactPolicy.builder()
|
||||
.organisation(org)
|
||||
.typePolitique(TypePolitiqueCommunication.BUREAU_SEULEMENT)
|
||||
.autoriserMembreVersMembre(false)
|
||||
.autoriserMembreVersRole(true)
|
||||
.autoriserNotesVocales(true)
|
||||
.build();
|
||||
policy.setId(UUID.randomUUID());
|
||||
|
||||
when(contactPolicyRepository.findByOrganisationId(ORG_ID)).thenReturn(Optional.of(policy));
|
||||
|
||||
ContactPolicyResponse result = messagingService.getPolitique(ORG_ID);
|
||||
|
||||
assertThat(result.getTypePolitique()).isEqualTo("BUREAU_SEULEMENT");
|
||||
assertThat(result.isAutoriserMembreVersMembre()).isFalse();
|
||||
}
|
||||
|
||||
// ── mettreAJourPolitique ──────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("mettreAJourPolitique — change typePolitique")
|
||||
void mettreAJourPolitique_changeType() {
|
||||
Organisation org = newOrg();
|
||||
ContactPolicy policy = ContactPolicy.builder()
|
||||
.organisation(org)
|
||||
.typePolitique(TypePolitiqueCommunication.OUVERT)
|
||||
.autoriserMembreVersMembre(true)
|
||||
.autoriserMembreVersRole(true)
|
||||
.autoriserNotesVocales(true)
|
||||
.build();
|
||||
policy.setId(UUID.randomUUID());
|
||||
|
||||
when(contactPolicyRepository.findByOrganisationId(ORG_ID)).thenReturn(Optional.of(policy));
|
||||
|
||||
MettreAJourPolitiqueRequest req = MettreAJourPolitiqueRequest.builder()
|
||||
.typePolitique("BUREAU_SEULEMENT")
|
||||
.autoriserMembreVersMembre(false)
|
||||
.build();
|
||||
|
||||
ContactPolicyResponse result = messagingService.mettreAJourPolitique(ORG_ID, req);
|
||||
|
||||
assertThat(result.getTypePolitique()).isEqualTo("BUREAU_SEULEMENT");
|
||||
assertThat(result.isAutoriserMembreVersMembre()).isFalse();
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private Membre newMembre(UUID id, String email, String prenom, String nom) {
|
||||
Membre m = new Membre();
|
||||
m.setId(id);
|
||||
m.setNumeroMembre("M-" + id.toString().substring(0, 4));
|
||||
m.setPrenom(prenom);
|
||||
m.setNom(nom);
|
||||
m.setEmail(email);
|
||||
m.setDateNaissance(LocalDate.now());
|
||||
return m;
|
||||
}
|
||||
|
||||
private Organisation newOrg() {
|
||||
Organisation org = new Organisation();
|
||||
org.setId(ORG_ID);
|
||||
org.setNom("Tontine Test");
|
||||
return org;
|
||||
}
|
||||
|
||||
private Conversation buildConversation(TypeConversation type) {
|
||||
Conversation conv = new Conversation();
|
||||
conv.setId(CONV_ID);
|
||||
conv.setOrganisation(newOrg());
|
||||
conv.setTypeConversation(type);
|
||||
conv.setStatut(StatutConversation.ACTIVE);
|
||||
conv.setNombreMessages(0);
|
||||
return conv;
|
||||
}
|
||||
|
||||
private Message buildMessage(Conversation conv) {
|
||||
Membre expediteur = newMembre(MEMBRE_ID, "membre@test.com", "Alpha", "Diallo");
|
||||
Message msg = new Message();
|
||||
msg.setId(MESSAGE_ID);
|
||||
msg.setConversation(conv);
|
||||
msg.setExpediteur(expediteur);
|
||||
msg.setTypeMessage(TypeContenu.TEXTE);
|
||||
msg.setContenu("Bonjour !");
|
||||
msg.setDateCreation(LocalDateTime.now());
|
||||
return msg;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> io.quarkus.hibernate.orm.panache.PanacheQuery<T> mockSingleResultQuery(T entity) {
|
||||
io.quarkus.hibernate.orm.panache.PanacheQuery<T> query =
|
||||
org.mockito.Mockito.mock(io.quarkus.hibernate.orm.panache.PanacheQuery.class);
|
||||
when(query.firstResult()).thenReturn(entity);
|
||||
return query;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,755 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.versement.request.DeclarerVersementManuelRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.request.InitierDepotEpargneRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.request.InitierVersementWaveRequest;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementGatewayResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementStatutResponse;
|
||||
import dev.lions.unionflow.server.api.dto.versement.response.VersementSummaryResponse;
|
||||
import dev.lions.unionflow.server.api.enums.paiement.StatutIntentionPaiement;
|
||||
import dev.lions.unionflow.server.entity.Cotisation;
|
||||
import dev.lions.unionflow.server.entity.IntentionPaiement;
|
||||
import dev.lions.unionflow.server.entity.Membre;
|
||||
import dev.lions.unionflow.server.entity.Organisation;
|
||||
import dev.lions.unionflow.server.entity.Versement;
|
||||
import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne;
|
||||
import dev.lions.unionflow.server.repository.IntentionPaiementRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreOrganisationRepository;
|
||||
import dev.lions.unionflow.server.repository.MembreRepository;
|
||||
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
|
||||
import dev.lions.unionflow.server.repository.VersementRepository;
|
||||
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
|
||||
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutException;
|
||||
import dev.lions.unionflow.server.service.WaveCheckoutService.WaveCheckoutSessionResponse;
|
||||
import io.quarkus.test.InjectMock;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.quarkus.test.security.TestSecurity;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour {@link VersementService}.
|
||||
*
|
||||
* Couverture : trouverParId, trouverParNumeroReference, listerParMembre,
|
||||
* calculerMontantTotalConfirmes, getMesVersements, validerVersement,
|
||||
* annulerVersement, initierVersementWave (succès + Wave error + cotisation
|
||||
* inconnue + cotisation d'un autre membre), declarerVersementManuel,
|
||||
* verifierStatutVersement (complété / expiré / en attente),
|
||||
* confirmerVersementWave (idempotence), toE164 (toutes branches).
|
||||
*/
|
||||
@QuarkusTest
|
||||
@DisplayName("VersementService")
|
||||
class VersementServiceTest {
|
||||
|
||||
@Inject
|
||||
VersementService versementService;
|
||||
|
||||
@InjectMock VersementRepository versementRepository;
|
||||
@InjectMock MembreRepository membreRepository;
|
||||
@InjectMock KeycloakService keycloakService;
|
||||
@InjectMock TypeReferenceRepository typeReferenceRepository;
|
||||
@InjectMock IntentionPaiementRepository intentionPaiementRepository;
|
||||
@InjectMock WaveCheckoutService waveCheckoutService;
|
||||
@InjectMock CompteEpargneRepository compteEpargneRepository;
|
||||
@InjectMock MembreOrganisationRepository membreOrganisationRepository;
|
||||
@InjectMock NotificationService notificationService;
|
||||
|
||||
private Membre testMembre;
|
||||
private Membre autreMembre;
|
||||
private Organisation testOrg;
|
||||
private Cotisation testCotisation;
|
||||
private Versement testVersement;
|
||||
private CompteEpargne testCompte;
|
||||
private EntityManager mockEm;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
testOrg = new Organisation();
|
||||
testOrg.setId(UUID.randomUUID());
|
||||
testOrg.setNom("Org Test");
|
||||
|
||||
testMembre = new Membre();
|
||||
testMembre.setId(UUID.randomUUID());
|
||||
testMembre.setNumeroMembre("M001");
|
||||
testMembre.setEmail("membre@test.com");
|
||||
testMembre.setPrenom("Jean");
|
||||
testMembre.setNom("Dupont");
|
||||
testMembre.setDateNaissance(LocalDate.of(1990, 1, 1));
|
||||
|
||||
autreMembre = new Membre();
|
||||
autreMembre.setId(UUID.randomUUID());
|
||||
autreMembre.setNumeroMembre("M002");
|
||||
autreMembre.setEmail("autre@test.com");
|
||||
autreMembre.setDateNaissance(LocalDate.of(1985, 5, 15));
|
||||
|
||||
testCotisation = new Cotisation();
|
||||
testCotisation.setId(UUID.randomUUID());
|
||||
testCotisation.setNumeroReference("COT-2026-001");
|
||||
testCotisation.setMembre(testMembre);
|
||||
testCotisation.setOrganisation(testOrg);
|
||||
testCotisation.setMontantDu(new BigDecimal("5000"));
|
||||
testCotisation.setCodeDevise("XOF");
|
||||
testCotisation.setStatut("EN_ATTENTE");
|
||||
testCotisation.setDateEcheance(LocalDate.now().plusDays(30));
|
||||
testCotisation.setTypeCotisation("MENSUELLE");
|
||||
testCotisation.setLibelle("Cotisation janvier 2026");
|
||||
testCotisation.setAnnee(2026);
|
||||
|
||||
testVersement = new Versement();
|
||||
testVersement.setId(UUID.randomUUID());
|
||||
testVersement.setNumeroReference("VRS-2026-001");
|
||||
testVersement.setMontant(new BigDecimal("5000"));
|
||||
testVersement.setCodeDevise("XOF");
|
||||
testVersement.setMethodePaiement("WAVE");
|
||||
testVersement.setStatutPaiement("EN_ATTENTE");
|
||||
testVersement.setMembre(testMembre);
|
||||
testVersement.setDatePaiement(LocalDateTime.now());
|
||||
|
||||
testCompte = new CompteEpargne();
|
||||
testCompte.setId(UUID.randomUUID());
|
||||
testCompte.setMembre(testMembre);
|
||||
testCompte.setOrganisation(testOrg);
|
||||
|
||||
mockEm = mock(EntityManager.class);
|
||||
when(versementRepository.getEntityManager()).thenReturn(mockEm);
|
||||
when(typeReferenceRepository.findByDomaineAndCode(anyString(), anyString()))
|
||||
.thenReturn(Optional.empty());
|
||||
when(keycloakService.getCurrentUserEmail()).thenReturn(testMembre.getEmail());
|
||||
}
|
||||
|
||||
// ── trouverParId ──────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("trouverParId — trouvé → retourne response")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void trouverParId_found() {
|
||||
when(versementRepository.findVersementById(testVersement.getId()))
|
||||
.thenReturn(Optional.of(testVersement));
|
||||
|
||||
VersementResponse r = versementService.trouverParId(testVersement.getId());
|
||||
|
||||
assertThat(r.getNumeroReference()).isEqualTo("VRS-2026-001");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("trouverParId — non trouvé → NotFoundException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void trouverParId_notFound() {
|
||||
when(versementRepository.findVersementById(any())).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> versementService.trouverParId(UUID.randomUUID()))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
// ── trouverParNumeroReference ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("trouverParNumeroReference — trouvé → response")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void trouverParNumeroReference_found() {
|
||||
when(versementRepository.findByNumeroReference("VRS-2026-001"))
|
||||
.thenReturn(Optional.of(testVersement));
|
||||
|
||||
VersementResponse r = versementService.trouverParNumeroReference("VRS-2026-001");
|
||||
assertThat(r).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("trouverParNumeroReference — non trouvé → NotFoundException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void trouverParNumeroReference_notFound() {
|
||||
when(versementRepository.findByNumeroReference(anyString())).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> versementService.trouverParNumeroReference("VRS-INCONNU"))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
// ── listerParMembre ───────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("listerParMembre — retourne liste non nulle")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void listerParMembre_returnsList() {
|
||||
when(versementRepository.findByMembreId(testMembre.getId()))
|
||||
.thenReturn(List.of(testVersement));
|
||||
|
||||
List<VersementSummaryResponse> list = versementService.listerParMembre(testMembre.getId());
|
||||
assertThat(list).hasSize(1);
|
||||
}
|
||||
|
||||
// ── calculerMontantTotalConfirmes ─────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("calculerMontantTotalConfirmes — délègue au repository")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void calculerMontantTotalConfirmes_delegates() {
|
||||
LocalDateTime debut = LocalDateTime.now().minusDays(30);
|
||||
LocalDateTime fin = LocalDateTime.now();
|
||||
when(versementRepository.calculerMontantTotalConfirmes(debut, fin))
|
||||
.thenReturn(new BigDecimal("15000"));
|
||||
|
||||
BigDecimal total = versementService.calculerMontantTotalConfirmes(debut, fin);
|
||||
assertThat(total).isEqualByComparingTo("15000");
|
||||
}
|
||||
|
||||
// ── getMesVersements ──────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("getMesVersements — retourne historique du membre connecté")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void getMesVersements_returnsHistory() {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
@SuppressWarnings("unchecked")
|
||||
TypedQuery<Versement> query = mock(TypedQuery.class);
|
||||
doReturn(query).when(mockEm).createQuery(anyString(), any());
|
||||
when(query.setParameter(anyString(), any())).thenReturn(query);
|
||||
when(query.setMaxResults(anyInt())).thenReturn(query);
|
||||
when(query.getResultList()).thenReturn(List.of(testVersement));
|
||||
|
||||
List<VersementSummaryResponse> list = versementService.getMesVersements(5);
|
||||
assertThat(list).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getMesVersements — membre non trouvé → NotFoundException")
|
||||
@TestSecurity(user = "inconnu@test.com", roles = {"MEMBRE"})
|
||||
void getMesVersements_memberNotFound() {
|
||||
when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> versementService.getMesVersements(5))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
// ── validerVersement ──────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("validerVersement — passe à CONFIRME")
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void validerVersement_setsConfirme() {
|
||||
when(versementRepository.findVersementById(testVersement.getId()))
|
||||
.thenReturn(Optional.of(testVersement));
|
||||
|
||||
VersementResponse r = versementService.validerVersement(testVersement.getId());
|
||||
assertThat(r.getStatutPaiement()).isEqualTo("CONFIRME");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("validerVersement — déjà confirmé → idempotent")
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void validerVersement_alreadyConfirme_idempotent() {
|
||||
testVersement.setStatutPaiement("CONFIRME");
|
||||
when(versementRepository.findVersementById(testVersement.getId()))
|
||||
.thenReturn(Optional.of(testVersement));
|
||||
|
||||
VersementResponse r = versementService.validerVersement(testVersement.getId());
|
||||
assertThat(r.getStatutPaiement()).isEqualTo("CONFIRME");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("validerVersement — non trouvé → NotFoundException")
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void validerVersement_notFound() {
|
||||
when(versementRepository.findVersementById(any())).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> versementService.validerVersement(UUID.randomUUID()))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
// ── annulerVersement ──────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("annulerVersement — passe à ANNULE")
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void annulerVersement_setsAnnule() {
|
||||
when(versementRepository.findVersementById(testVersement.getId()))
|
||||
.thenReturn(Optional.of(testVersement));
|
||||
|
||||
VersementResponse r = versementService.annulerVersement(testVersement.getId());
|
||||
assertThat(r.getStatutPaiement()).isEqualTo("ANNULE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("annulerVersement — déjà ANNULE → IllegalStateException")
|
||||
@TestSecurity(user = "admin@test.com", roles = {"ADMIN"})
|
||||
void annulerVersement_alreadyAnnule() {
|
||||
testVersement.setStatutPaiement("ANNULE");
|
||||
when(versementRepository.findVersementById(testVersement.getId()))
|
||||
.thenReturn(Optional.of(testVersement));
|
||||
|
||||
assertThatThrownBy(() -> versementService.annulerVersement(testVersement.getId()))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
// ── initierVersementWave ──────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("initierVersementWave — succès Wave → retourne waveLaunchUrl")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierVersementWave_success() throws WaveCheckoutException {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
when(mockEm.find(Cotisation.class, testCotisation.getId()))
|
||||
.thenReturn(testCotisation);
|
||||
when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");
|
||||
|
||||
WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("cos-abc123", "wave://checkout/abc123");
|
||||
when(waveCheckoutService.createSession(
|
||||
anyString(), anyString(), anyString(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(session);
|
||||
doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
|
||||
doAnswer(inv -> null).when(versementRepository).persist(any(Versement.class));
|
||||
when(mockEm.merge(any())).thenReturn(testCotisation);
|
||||
|
||||
InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
|
||||
.cotisationId(testCotisation.getId())
|
||||
.numeroTelephone("771234567")
|
||||
.build();
|
||||
|
||||
VersementGatewayResponse r = versementService.initierVersementWave(request);
|
||||
|
||||
assertThat(r.getWaveLaunchUrl()).isEqualTo("wave://checkout/abc123");
|
||||
assertThat(r.getWaveCheckoutSessionId()).isEqualTo("cos-abc123");
|
||||
assertThat(r.getMontant()).isEqualByComparingTo("5000");
|
||||
assertThat(r.getClientReference()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("initierVersementWave — cotisation inconnue → NotFoundException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierVersementWave_cotisationInconnue() {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
when(mockEm.find(Cotisation.class, any())).thenReturn(null);
|
||||
|
||||
InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
|
||||
.cotisationId(UUID.randomUUID())
|
||||
.numeroTelephone("771234567")
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> versementService.initierVersementWave(request))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("initierVersementWave — cotisation d'un autre membre → IllegalArgumentException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierVersementWave_cotisationAutreMembre() {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
testCotisation.setMembre(autreMembre);
|
||||
when(mockEm.find(Cotisation.class, testCotisation.getId()))
|
||||
.thenReturn(testCotisation);
|
||||
|
||||
InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
|
||||
.cotisationId(testCotisation.getId())
|
||||
.numeroTelephone("771234567")
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> versementService.initierVersementWave(request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("initierVersementWave — Wave API error → BadRequestException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierVersementWave_waveApiError() throws WaveCheckoutException {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
when(mockEm.find(Cotisation.class, testCotisation.getId()))
|
||||
.thenReturn(testCotisation);
|
||||
when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");
|
||||
doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
|
||||
doThrow(new WaveCheckoutException("Wave down"))
|
||||
.when(waveCheckoutService).createSession(
|
||||
anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
|
||||
InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
|
||||
.cotisationId(testCotisation.getId())
|
||||
.numeroTelephone("771234567")
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> versementService.initierVersementWave(request))
|
||||
.isInstanceOf(jakarta.ws.rs.BadRequestException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("initierVersementWave — sans numéro téléphone (web QR) → successUrl web")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierVersementWave_noPhone_webContext() throws WaveCheckoutException {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
when(mockEm.find(Cotisation.class, testCotisation.getId()))
|
||||
.thenReturn(testCotisation);
|
||||
when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");
|
||||
|
||||
WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("cos-web", "wave://checkout/web");
|
||||
when(waveCheckoutService.createSession(
|
||||
anyString(), anyString(), anyString(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(session);
|
||||
doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
|
||||
doAnswer(inv -> null).when(versementRepository).persist(any(Versement.class));
|
||||
when(mockEm.merge(any())).thenReturn(testCotisation);
|
||||
|
||||
InitierVersementWaveRequest request = InitierVersementWaveRequest.builder()
|
||||
.cotisationId(testCotisation.getId())
|
||||
.build(); // pas de numéro → web
|
||||
|
||||
VersementGatewayResponse r = versementService.initierVersementWave(request);
|
||||
assertThat(r.getWaveLaunchUrl()).isEqualTo("wave://checkout/web");
|
||||
}
|
||||
|
||||
// ── initierDepotEpargneEnLigne ────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("initierDepotEpargneEnLigne — succès Wave")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierDepotEpargne_success() throws WaveCheckoutException {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
when(compteEpargneRepository.findByIdOptional(testCompte.getId()))
|
||||
.thenReturn(Optional.of(testCompte));
|
||||
when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");
|
||||
|
||||
WaveCheckoutSessionResponse session = new WaveCheckoutSessionResponse("cos-epargne", "wave://checkout/epargne");
|
||||
when(waveCheckoutService.createSession(
|
||||
anyString(), anyString(), anyString(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(session);
|
||||
doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
|
||||
|
||||
InitierDepotEpargneRequest request = InitierDepotEpargneRequest.builder()
|
||||
.compteId(testCompte.getId())
|
||||
.montant(new BigDecimal("10000"))
|
||||
.numeroTelephone("771234567")
|
||||
.build();
|
||||
|
||||
VersementGatewayResponse r = versementService.initierDepotEpargneEnLigne(request);
|
||||
assertThat(r.getWaveLaunchUrl()).isEqualTo("wave://checkout/epargne");
|
||||
assertThat(r.getMontant()).isEqualByComparingTo("10000");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("initierDepotEpargneEnLigne — compte inconnu → NotFoundException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierDepotEpargne_compteInconnu() {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
when(compteEpargneRepository.findByIdOptional(any())).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> versementService.initierDepotEpargneEnLigne(
|
||||
InitierDepotEpargneRequest.builder()
|
||||
.compteId(UUID.randomUUID())
|
||||
.montant(BigDecimal.TEN)
|
||||
.build()))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("initierDepotEpargneEnLigne — compte d'un autre membre → IllegalArgumentException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierDepotEpargne_compteAutreMembre() {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
testCompte.setMembre(autreMembre);
|
||||
when(compteEpargneRepository.findByIdOptional(testCompte.getId()))
|
||||
.thenReturn(Optional.of(testCompte));
|
||||
|
||||
assertThatThrownBy(() -> versementService.initierDepotEpargneEnLigne(
|
||||
InitierDepotEpargneRequest.builder()
|
||||
.compteId(testCompte.getId())
|
||||
.montant(BigDecimal.TEN)
|
||||
.build()))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("initierDepotEpargneEnLigne — Wave error → BadRequestException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void initierDepotEpargne_waveError() throws WaveCheckoutException {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
when(compteEpargneRepository.findByIdOptional(testCompte.getId()))
|
||||
.thenReturn(Optional.of(testCompte));
|
||||
when(waveCheckoutService.getRedirectBaseUrl()).thenReturn("https://api.test.com");
|
||||
doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
|
||||
doThrow(new WaveCheckoutException("Wave down"))
|
||||
.when(waveCheckoutService).createSession(
|
||||
anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
|
||||
assertThatThrownBy(() -> versementService.initierDepotEpargneEnLigne(
|
||||
InitierDepotEpargneRequest.builder()
|
||||
.compteId(testCompte.getId())
|
||||
.montant(BigDecimal.TEN)
|
||||
.build()))
|
||||
.isInstanceOf(jakarta.ws.rs.BadRequestException.class);
|
||||
}
|
||||
|
||||
// ── declarerVersementManuel ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("declarerVersementManuel — créé EN_ATTENTE_VALIDATION")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void declarerVersementManuel_success() {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
@SuppressWarnings("unchecked")
|
||||
TypedQuery<Cotisation> query = mock(TypedQuery.class);
|
||||
doReturn(query).when(mockEm).createQuery(anyString(), any());
|
||||
when(query.setParameter(anyString(), any())).thenReturn(query);
|
||||
when(query.getResultList()).thenReturn(List.of(testCotisation));
|
||||
doAnswer(inv -> null).when(versementRepository).persist(any(Versement.class));
|
||||
when(membreOrganisationRepository.findFirstByMembreId(any()))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
DeclarerVersementManuelRequest request = DeclarerVersementManuelRequest.builder()
|
||||
.cotisationId(testCotisation.getId())
|
||||
.methodePaiement("ESPECES")
|
||||
.commentaire("Remis en main propre")
|
||||
.build();
|
||||
|
||||
VersementResponse r = versementService.declarerVersementManuel(request);
|
||||
assertThat(r.getStatutPaiement()).isEqualTo("EN_ATTENTE_VALIDATION");
|
||||
assertThat(r.getMethodePaiement()).isEqualTo("ESPECES");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("declarerVersementManuel — cotisation inconnue → NotFoundException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void declarerVersementManuel_cotisationInconnue() {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
@SuppressWarnings("unchecked")
|
||||
TypedQuery<Cotisation> query = mock(TypedQuery.class);
|
||||
doReturn(query).when(mockEm).createQuery(anyString(), any());
|
||||
when(query.setParameter(anyString(), any())).thenReturn(query);
|
||||
when(query.getResultList()).thenReturn(Collections.emptyList());
|
||||
|
||||
assertThatThrownBy(() -> versementService.declarerVersementManuel(
|
||||
DeclarerVersementManuelRequest.builder()
|
||||
.cotisationId(UUID.randomUUID())
|
||||
.methodePaiement("ESPECES")
|
||||
.build()))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("declarerVersementManuel — cotisation d'un autre membre → IllegalArgumentException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void declarerVersementManuel_cotisationAutreMembre() {
|
||||
when(membreRepository.findByEmail("membre@test.com"))
|
||||
.thenReturn(Optional.of(testMembre));
|
||||
testCotisation.setMembre(autreMembre);
|
||||
@SuppressWarnings("unchecked")
|
||||
TypedQuery<Cotisation> query = mock(TypedQuery.class);
|
||||
doReturn(query).when(mockEm).createQuery(anyString(), any());
|
||||
when(query.setParameter(anyString(), any())).thenReturn(query);
|
||||
when(query.getResultList()).thenReturn(List.of(testCotisation));
|
||||
|
||||
assertThatThrownBy(() -> versementService.declarerVersementManuel(
|
||||
DeclarerVersementManuelRequest.builder()
|
||||
.cotisationId(testCotisation.getId())
|
||||
.methodePaiement("ESPECES")
|
||||
.build()))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
// ── verifierStatutVersement ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("verifierStatutVersement — intention inconnue → NotFoundException")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void verifierStatutVersement_notFound() {
|
||||
when(intentionPaiementRepository.findById(any())).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> versementService.verifierStatutVersement(UUID.randomUUID()))
|
||||
.isInstanceOf(NotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("verifierStatutVersement — déjà COMPLETEE → confirme=true")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void verifierStatutVersement_completee() {
|
||||
IntentionPaiement intention = buildIntention(StatutIntentionPaiement.COMPLETEE);
|
||||
when(intentionPaiementRepository.findById(intention.getId()))
|
||||
.thenReturn(intention);
|
||||
|
||||
VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
|
||||
assertThat(r.isConfirme()).isTrue();
|
||||
assertThat(r.getMessage()).contains("confirmé");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("verifierStatutVersement — EXPIREE → confirme=false")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void verifierStatutVersement_expiree() {
|
||||
IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EXPIREE);
|
||||
when(intentionPaiementRepository.findById(intention.getId()))
|
||||
.thenReturn(intention);
|
||||
|
||||
VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
|
||||
assertThat(r.isConfirme()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("verifierStatutVersement — ECHOUEE → confirme=false")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void verifierStatutVersement_echouee() {
|
||||
IntentionPaiement intention = buildIntention(StatutIntentionPaiement.ECHOUEE);
|
||||
when(intentionPaiementRepository.findById(intention.getId()))
|
||||
.thenReturn(intention);
|
||||
|
||||
VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
|
||||
assertThat(r.isConfirme()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("verifierStatutVersement — session expirée localement → EXPIREE")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void verifierStatutVersement_localExpiry() {
|
||||
IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS);
|
||||
intention.setDateExpiration(LocalDateTime.now().minusMinutes(5));
|
||||
when(intentionPaiementRepository.findById(intention.getId()))
|
||||
.thenReturn(intention);
|
||||
|
||||
VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
|
||||
assertThat(r.getStatut()).isEqualTo("EXPIREE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("verifierStatutVersement — EN_COURS sans session Wave → en attente")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void verifierStatutVersement_enCoursNoSession() {
|
||||
IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS);
|
||||
intention.setDateExpiration(LocalDateTime.now().plusMinutes(25));
|
||||
when(intentionPaiementRepository.findById(intention.getId()))
|
||||
.thenReturn(intention);
|
||||
|
||||
VersementStatutResponse r = versementService.verifierStatutVersement(intention.getId());
|
||||
assertThat(r.isConfirme()).isFalse();
|
||||
assertThat(r.getMessage()).containsIgnoringCase("attente");
|
||||
}
|
||||
|
||||
// ── confirmerVersementWave ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("confirmerVersementWave — déjà COMPLETEE → idempotent")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void confirmerVersementWave_alreadyCompletee_idempotent() {
|
||||
IntentionPaiement intention = buildIntention(StatutIntentionPaiement.COMPLETEE);
|
||||
// Ne doit pas appeler persist
|
||||
versementService.confirmerVersementWave(intention, null);
|
||||
// Pas d'exception = idempotence OK
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("confirmerVersementWave — objetsCibles null → passe sans erreur")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void confirmerVersementWave_nullObjetsCibles() {
|
||||
IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS);
|
||||
intention.setObjetsCibles(null);
|
||||
doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
|
||||
|
||||
versementService.confirmerVersementWave(intention, "TCN-123");
|
||||
|
||||
assertThat(intention.getStatut()).isEqualTo(StatutIntentionPaiement.COMPLETEE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("confirmerVersementWave — JSON cotisation → mise à jour cotisation")
|
||||
@TestSecurity(user = "membre@test.com", roles = {"MEMBRE"})
|
||||
void confirmerVersementWave_reconcilesCotisation() {
|
||||
UUID cotisationId = testCotisation.getId();
|
||||
IntentionPaiement intention = buildIntention(StatutIntentionPaiement.EN_COURS);
|
||||
intention.setObjetsCibles(
|
||||
"[{\"type\":\"COTISATION\",\"id\":\"" + cotisationId + "\",\"montant\":5000}]");
|
||||
doAnswer(inv -> null).when(intentionPaiementRepository).persist(any(IntentionPaiement.class));
|
||||
when(mockEm.find(Cotisation.class, cotisationId)).thenReturn(testCotisation);
|
||||
when(mockEm.merge(any())).thenReturn(testCotisation);
|
||||
|
||||
versementService.confirmerVersementWave(intention, "TCN-ABC");
|
||||
|
||||
assertThat(testCotisation.getStatut()).isEqualTo("PAYEE");
|
||||
assertThat(testCotisation.getMontantPaye()).isEqualByComparingTo("5000");
|
||||
}
|
||||
|
||||
// ── toE164 ────────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("toE164 — null → null")
|
||||
void toE164_null() {
|
||||
assertThat(VersementService.toE164(null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toE164 — vide → null")
|
||||
void toE164_blank() {
|
||||
assertThat(VersementService.toE164(" ")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toE164 — 9 chiffres commençant par 7 → +221 prefix")
|
||||
void toE164_9digits_7prefix() {
|
||||
assertThat(VersementService.toE164("771234567")).isEqualTo("+221771234567");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toE164 — 9 chiffres commençant par 0 → +221 + supprime 0")
|
||||
void toE164_9digits_0prefix() {
|
||||
assertThat(VersementService.toE164("071234567")).isEqualTo("+22171234567");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toE164 — déjà avec indicatif 221 → ajoute +")
|
||||
void toE164_with221prefix() {
|
||||
assertThat(VersementService.toE164("221771234567")).isEqualTo("+221771234567");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toE164 — déjà au format E.164 (+221...) → conservé")
|
||||
void toE164_alreadyE164() {
|
||||
assertThat(VersementService.toE164("+221771234567")).isEqualTo("+221771234567");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("toE164 — format non reconnu → + + chiffres")
|
||||
void toE164_unknown() {
|
||||
assertThat(VersementService.toE164("0033612345678")).startsWith("+");
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private IntentionPaiement buildIntention(StatutIntentionPaiement statut) {
|
||||
IntentionPaiement i = new IntentionPaiement();
|
||||
i.setId(UUID.randomUUID());
|
||||
i.setStatut(statut);
|
||||
i.setMontantTotal(new BigDecimal("5000"));
|
||||
i.setCodeDevise("XOF");
|
||||
i.setUtilisateur(testMembre);
|
||||
i.setDateExpiration(LocalDateTime.now().plusMinutes(30));
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
|
||||
https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
|
||||
version="4.0"
|
||||
bean-discovery-mode="annotated">
|
||||
</beans>
|
||||
@@ -1,63 +0,0 @@
|
||||
# ============================================================================
|
||||
# UnionFlow Server — Profil PROD
|
||||
# Chargé automatiquement quand le profil "prod" est actif
|
||||
# Surcharge application.properties — sans préfixes %prod.
|
||||
# ============================================================================
|
||||
|
||||
# Base de données PostgreSQL — Production (variables d'environnement obligatoires)
|
||||
quarkus.datasource.username=${DB_USERNAME}
|
||||
quarkus.datasource.password=${DB_PASSWORD}
|
||||
quarkus.datasource.jdbc.url=${DB_URL}
|
||||
quarkus.datasource.jdbc.min-size=5
|
||||
quarkus.datasource.jdbc.max-size=20
|
||||
quarkus.datasource.jdbc.acquisition-timeout=5
|
||||
quarkus.datasource.jdbc.idle-removal-interval=PT2M
|
||||
quarkus.datasource.jdbc.max-lifetime=PT30M
|
||||
|
||||
# Hibernate — Validate uniquement (Flyway gère le schéma)
|
||||
quarkus.hibernate-orm.database.generation=validate
|
||||
quarkus.hibernate-orm.statistics=false
|
||||
|
||||
# CORS — strict en production
|
||||
quarkus.http.cors.origins=${CORS_ORIGINS:https://unionflow.lions.dev,https://security.lions.dev}
|
||||
quarkus.http.cors.access-control-allow-credentials=true
|
||||
|
||||
# WebSocket — public (auth gérée dans le handshake)
|
||||
quarkus.http.auth.permission.websocket.paths=/ws/*
|
||||
quarkus.http.auth.permission.websocket.policy=permit
|
||||
|
||||
# Keycloak / OIDC — Production
|
||||
quarkus.oidc.tenant-enabled=true
|
||||
quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/unionflow}
|
||||
quarkus.oidc.client-id=unionflow-server
|
||||
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
|
||||
quarkus.oidc.tls.verification=required
|
||||
|
||||
# OpenAPI — serveur prod
|
||||
quarkus.smallrye-openapi.servers=https://api.lions.dev/unionflow
|
||||
quarkus.smallrye-openapi.oidc-open-id-connect-url=${quarkus.oidc.auth-server-url}/.well-known/openid-configuration
|
||||
|
||||
# Swagger UI — désactivé en production
|
||||
quarkus.swagger-ui.always-include=false
|
||||
|
||||
# Logging — fichier en production
|
||||
quarkus.log.file.enable=true
|
||||
quarkus.log.file.path=/var/log/unionflow/server.log
|
||||
quarkus.log.file.rotation.max-file-size=10M
|
||||
quarkus.log.file.rotation.max-backup-index=5
|
||||
quarkus.log.category."org.jboss.resteasy".level=WARN
|
||||
|
||||
# REST Client lions-user-manager
|
||||
quarkus.rest-client.lions-user-manager-api.url=${LIONS_USER_MANAGER_URL:http://lions-user-manager:8081}
|
||||
|
||||
# Wave Money — Production
|
||||
wave.environment=production
|
||||
|
||||
# Email — Production
|
||||
quarkus.mailer.from=${MAIL_FROM:noreply@unionflow.lions.dev}
|
||||
quarkus.mailer.host=${MAIL_HOST:smtp.lions.dev}
|
||||
quarkus.mailer.port=${MAIL_PORT:587}
|
||||
quarkus.mailer.username=${MAIL_USERNAME:}
|
||||
quarkus.mailer.password=${MAIL_PASSWORD:}
|
||||
quarkus.mailer.start-tls=REQUIRED
|
||||
quarkus.mailer.ssl=false
|
||||
@@ -1,48 +0,0 @@
|
||||
# Configuration UnionFlow Server - Profil Test
|
||||
# Ce fichier est chargé automatiquement quand le profil 'test' est actif
|
||||
|
||||
# Configuration Base de données H2 pour tests
|
||||
quarkus.datasource.db-kind=h2
|
||||
quarkus.datasource.username=sa
|
||||
quarkus.datasource.password=
|
||||
quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL;NON_KEYWORDS=MONTH,YEAR
|
||||
|
||||
# Configuration Hibernate pour tests
|
||||
quarkus.hibernate-orm.database.generation=update
|
||||
# Désactiver complètement l'exécution des scripts SQL au démarrage
|
||||
quarkus.hibernate-orm.sql-load-script=no-file
|
||||
# Empêcher Hibernate d'exécuter les scripts SQL automatiquement
|
||||
# Note: Ne pas définir quarkus.hibernate-orm.sql-load-script car une chaîne vide peut causer des problèmes
|
||||
|
||||
# Configuration Flyway pour tests (désactivé complètement)
|
||||
quarkus.flyway.migrate-at-start=false
|
||||
quarkus.flyway.enabled=false
|
||||
quarkus.flyway.baseline-on-migrate=false
|
||||
# Note: Ne pas définir quarkus.flyway.locations car une chaîne vide cause une erreur de configuration
|
||||
|
||||
# Configuration Keycloak pour tests (désactivé)
|
||||
quarkus.oidc.tenant-enabled=false
|
||||
quarkus.keycloak.policy-enforcer.enable=false
|
||||
|
||||
# Configuration HTTP pour tests
|
||||
quarkus.http.port=0
|
||||
quarkus.http.test-port=0
|
||||
|
||||
# Wave — mock pour tests
|
||||
wave.mock.enabled=true
|
||||
wave.api.key=test-wave-api-key-for-unit-tests
|
||||
wave.api.secret=test-wave-api-secret-for-unit-tests
|
||||
wave.redirect.base.url=http://localhost:8080
|
||||
|
||||
# Kafka — in-memory connector pour les tests (pas de broker Kafka requis)
|
||||
mp.messaging.outgoing.finance-approvals-out.connector=smallrye-in-memory
|
||||
mp.messaging.outgoing.dashboard-stats-out.connector=smallrye-in-memory
|
||||
mp.messaging.outgoing.notifications-out.connector=smallrye-in-memory
|
||||
mp.messaging.outgoing.members-events-out.connector=smallrye-in-memory
|
||||
mp.messaging.outgoing.contributions-events-out.connector=smallrye-in-memory
|
||||
mp.messaging.incoming.finance-approvals-in.connector=smallrye-in-memory
|
||||
mp.messaging.incoming.dashboard-stats-in.connector=smallrye-in-memory
|
||||
mp.messaging.incoming.notifications-in.connector=smallrye-in-memory
|
||||
mp.messaging.incoming.members-events-in.connector=smallrye-in-memory
|
||||
mp.messaging.incoming.contributions-events-in.connector=smallrye-in-memory
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
# ============================================================================
|
||||
# UnionFlow Server — Configuration commune (tous profils)
|
||||
# Chargée en premier, les fichiers application-{profil}.properties surchargent
|
||||
# ============================================================================
|
||||
|
||||
quarkus.application.name=unionflow-server
|
||||
quarkus.application.version=1.0.0
|
||||
|
||||
# Configuration HTTP
|
||||
quarkus.http.port=8085
|
||||
quarkus.http.host=0.0.0.0
|
||||
quarkus.http.limits.max-body-size=10M
|
||||
quarkus.http.limits.max-header-size=16K
|
||||
|
||||
# Configuration Datasource — db-kind est une propriété build-time (commune à tous profils)
|
||||
# Les valeurs réelles sont surchargées par application-dev.properties et application-prod.properties
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.username=${DB_USERNAME:unionflow}
|
||||
quarkus.datasource.password=${DB_PASSWORD:changeme}
|
||||
quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://localhost:5432/unionflow}
|
||||
|
||||
# Configuration CORS
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
|
||||
quarkus.http.cors.headers=Content-Type,Authorization
|
||||
|
||||
# Chemins publics
|
||||
quarkus.http.auth.permission.public.paths=/health,/q/*,/favicon.ico,/auth/callback,/auth/*
|
||||
quarkus.http.auth.permission.public.policy=permit
|
||||
|
||||
# Configuration Hibernate — base commune
|
||||
quarkus.hibernate-orm.database.generation=update
|
||||
quarkus.hibernate-orm.log.sql=false
|
||||
quarkus.hibernate-orm.jdbc.timezone=UTC
|
||||
quarkus.hibernate-orm.metrics.enabled=false
|
||||
|
||||
# Configuration Flyway — base commune
|
||||
quarkus.flyway.migrate-at-start=true
|
||||
quarkus.flyway.baseline-on-migrate=true
|
||||
quarkus.flyway.baseline-version=0
|
||||
|
||||
# Configuration Keycloak OIDC — base commune
|
||||
quarkus.oidc.application-type=service
|
||||
quarkus.oidc.roles.role-claim-path=realm_access/roles
|
||||
|
||||
# Keycloak Policy Enforcer (PERMISSIVE — sécurité gérée par @RolesAllowed)
|
||||
quarkus.keycloak.policy-enforcer.enable=false
|
||||
quarkus.keycloak.policy-enforcer.lazy-load-paths=true
|
||||
quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE
|
||||
|
||||
# Configuration OpenAPI
|
||||
quarkus.smallrye-openapi.info-title=UnionFlow Server API
|
||||
quarkus.smallrye-openapi.info-version=1.0.0
|
||||
quarkus.smallrye-openapi.info-description=API REST pour la gestion d'union avec authentification Keycloak
|
||||
quarkus.smallrye-openapi.security-scheme=oidc
|
||||
quarkus.smallrye-openapi.security-scheme-name=Keycloak
|
||||
quarkus.smallrye-openapi.security-scheme-description=Authentification Bearer JWT via Keycloak
|
||||
|
||||
# Swagger UI
|
||||
quarkus.swagger-ui.always-include=true
|
||||
quarkus.swagger-ui.path=/swagger-ui
|
||||
quarkus.swagger-ui.doc-expansion=list
|
||||
quarkus.swagger-ui.filter=true
|
||||
quarkus.swagger-ui.deep-linking=true
|
||||
quarkus.swagger-ui.operations-sorter=alpha
|
||||
quarkus.swagger-ui.tags-sorter=alpha
|
||||
|
||||
# Health
|
||||
quarkus.smallrye-health.root-path=/health
|
||||
|
||||
# Logging — base commune
|
||||
quarkus.log.console.enable=true
|
||||
quarkus.log.console.level=INFO
|
||||
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n
|
||||
quarkus.log.category."dev.lions.unionflow".level=INFO
|
||||
quarkus.log.category."org.hibernate".level=WARN
|
||||
quarkus.log.category."io.quarkus".level=INFO
|
||||
|
||||
# Arc / MapStruct
|
||||
quarkus.arc.remove-unused-beans=false
|
||||
quarkus.arc.unremovable-types=dev.lions.unionflow.server.mapper.**
|
||||
|
||||
# Jandex
|
||||
quarkus.index-dependency.unionflow-server-api.group-id=dev.lions.unionflow
|
||||
quarkus.index-dependency.unionflow-server-api.artifact-id=unionflow-server-api
|
||||
|
||||
# REST Client lions-user-manager
|
||||
quarkus.rest-client.lions-user-manager-api.url=${LIONS_USER_MANAGER_URL:http://localhost:8081}
|
||||
|
||||
# Wave Money — Checkout API (https://docs.wave.com/checkout)
|
||||
# Test : WAVE_API_KEY vide ou absent + wave.mock.enabled=true pour mocker Wave
|
||||
wave.api.key=${WAVE_API_KEY: }
|
||||
wave.api.secret=${WAVE_API_SECRET: }
|
||||
wave.api.base.url=${WAVE_API_BASE_URL:https://api.wave.com/v1}
|
||||
wave.environment=${WAVE_ENVIRONMENT:sandbox}
|
||||
wave.webhook.secret=${WAVE_WEBHOOK_SECRET: }
|
||||
# URLs de redirection (https en prod). Défaut dev: http://localhost:8080
|
||||
wave.redirect.base.url=${WAVE_REDIRECT_BASE_URL:http://localhost:8080}
|
||||
# Mock Wave (tests) : true = pas d'appel API, validation simulée. Si api.key vide, mock auto.
|
||||
wave.mock.enabled=${WAVE_MOCK_ENABLED:false}
|
||||
# Schéma deep link pour le retour vers l'app mobile (ex: unionflow)
|
||||
wave.deep.link.scheme=${WAVE_DEEP_LINK_SCHEME:unionflow}
|
||||
|
||||
# ============================================================================
|
||||
# Kafka Event Streaming Configuration
|
||||
# ============================================================================
|
||||
|
||||
# Kafka Bootstrap Servers
|
||||
kafka.bootstrap.servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
|
||||
|
||||
# Producer Channels (Outgoing)
|
||||
mp.messaging.outgoing.finance-approvals-out.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.finance-approvals-out.topic=unionflow.finance.approvals
|
||||
mp.messaging.outgoing.finance-approvals-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.finance-approvals-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
|
||||
mp.messaging.outgoing.dashboard-stats-out.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.dashboard-stats-out.topic=unionflow.dashboard.stats
|
||||
mp.messaging.outgoing.dashboard-stats-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.dashboard-stats-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
|
||||
mp.messaging.outgoing.notifications-out.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.notifications-out.topic=unionflow.notifications.user
|
||||
mp.messaging.outgoing.notifications-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.notifications-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
|
||||
mp.messaging.outgoing.members-events-out.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.members-events-out.topic=unionflow.members.events
|
||||
mp.messaging.outgoing.members-events-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.members-events-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
|
||||
mp.messaging.outgoing.contributions-events-out.connector=smallrye-kafka
|
||||
mp.messaging.outgoing.contributions-events-out.topic=unionflow.contributions.events
|
||||
mp.messaging.outgoing.contributions-events-out.value.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
mp.messaging.outgoing.contributions-events-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer
|
||||
|
||||
# Consumer Channels (Incoming)
|
||||
mp.messaging.incoming.finance-approvals-in.connector=smallrye-kafka
|
||||
mp.messaging.incoming.finance-approvals-in.topic=unionflow.finance.approvals
|
||||
mp.messaging.incoming.finance-approvals-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.finance-approvals-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.finance-approvals-in.group.id=unionflow-websocket-server
|
||||
|
||||
mp.messaging.incoming.dashboard-stats-in.connector=smallrye-kafka
|
||||
mp.messaging.incoming.dashboard-stats-in.topic=unionflow.dashboard.stats
|
||||
mp.messaging.incoming.dashboard-stats-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.dashboard-stats-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.dashboard-stats-in.group.id=unionflow-websocket-server
|
||||
|
||||
mp.messaging.incoming.notifications-in.connector=smallrye-kafka
|
||||
mp.messaging.incoming.notifications-in.topic=unionflow.notifications.user
|
||||
mp.messaging.incoming.notifications-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.notifications-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.notifications-in.group.id=unionflow-websocket-server
|
||||
|
||||
mp.messaging.incoming.members-events-in.connector=smallrye-kafka
|
||||
mp.messaging.incoming.members-events-in.topic=unionflow.members.events
|
||||
mp.messaging.incoming.members-events-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.members-events-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.members-events-in.group.id=unionflow-websocket-server
|
||||
|
||||
mp.messaging.incoming.contributions-events-in.connector=smallrye-kafka
|
||||
mp.messaging.incoming.contributions-events-in.topic=unionflow.contributions.events
|
||||
mp.messaging.incoming.contributions-events-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.contributions-events-in.key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
|
||||
mp.messaging.incoming.contributions-events-in.group.id=unionflow-websocket-server
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user