From 5fa4711a8ff312297cd3a62ddeabb4b43920fcff Mon Sep 17 00:00:00 2001 From: dahoud <41957584+DahoudG@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:51:10 +0000 Subject: [PATCH] =?UTF-8?q?feat(api):=20DTOs=20v3.0=20=E2=80=94=20membre?= =?UTF-8?q?=20computed=20fields,=20CreateMembreRequest=20builder,=20souscr?= =?UTF-8?q?iption=20DTOs,=20RBAC=20enums=20enrichis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MembreResponse: ajout getInitiales(), getDateAdhesionFormatee(), getStatut(), getSeverity() computed - CotisationResponse: alias getMethodePaiementLibelle() - CreateMembreRequest: builder pattern, organisationId UUID - Nouveaux DTOs souscription: FormuleAbonnementResponse, SouscriptionDemandeRequest, SouscriptionStatutResponse - Nouveaux enums: PlageMembres, StatutValidationSouscription, TypeOrganisationFacturation - StatutMembre enrichi (RADIE, ARCHIVE, INVITE, EN_ATTENTE_VALIDATION) - TypeOrganisation, TypeFormule, StatutSouscription mis à jour --- .../server/api/dto/common/ErrorResponse.java | 21 ++ .../response/CotisationResponse.java | 26 ++ .../evenement/response/EvenementResponse.java | 73 +++++ .../membre/request/CreateMembreRequest.java | 2 +- .../membre/request/UpdateMembreRequest.java | 2 +- .../dto/membre/response/MembreResponse.java | 68 ++++- .../response/MembreSummaryResponse.java | 110 +++++++- .../request/CreateOrganisationRequest.java | 9 +- .../request/UpdateOrganisationRequest.java | 9 +- .../InitierPaiementEnLigneRequest.java | 2 +- .../response/IntentionStatutResponse.java | 24 ++ .../FormuleAbonnementResponse.java | 128 +++++++++ .../SouscriptionDemandeRequest.java | 79 ++++++ .../SouscriptionStatutResponse.java | 155 ++++++++++ .../api/enums/abonnement/PlageMembres.java | 64 +++++ .../enums/abonnement/StatutSouscription.java | 1 + .../StatutValidationSouscription.java | 43 +++ .../api/enums/abonnement/TypeFormule.java | 20 +- .../TypeOrganisationFacturation.java | 51 ++++ .../abonnement/TypePeriodeAbonnement.java | 46 ++- .../server/api/enums/membre/StatutMembre.java | 4 +- .../enums/organisation/TypeOrganisation.java | 13 +- .../unionflow/server/api/TestDataFactory.java | 2 +- .../response/CotisationResponseTest.java | 96 +++++++ .../response/EvenementResponseTest.java | 255 +++++++++++++++++ .../dto/membre/MembreSearchResultDTOTest.java | 2 +- .../dto/membre/MembreSummaryResponseTest.java | 266 ++++++++++++++++++ .../request/CreateMembreRequestTest.java | 2 +- .../CreateOrganisationRequestTest.java | 67 +++++ .../UpdateOrganisationRequestTest.java | 51 ++++ .../InitierPaiementEnLigneRequestTest.java | 4 +- .../response/IntentionStatutResponseTest.java | 112 ++++++++ .../souscription/SouscriptionDtosTest.java | 172 +++++++++++ .../api/enums/EnumsRefactoringTest.java | 3 +- .../enums/abonnement/PlageMembresTest.java | 125 ++++++++ .../abonnement/StatutSouscriptionTest.java | 4 +- .../StatutValidationSouscriptionTest.java | 89 ++++++ .../api/enums/abonnement/TypeFormuleTest.java | 12 +- .../TypeOrganisationFacturationTest.java | 124 ++++++++ .../abonnement/TypePeriodeAbonnementTest.java | 30 +- .../api/enums/membre/StatutMembreTest.java | 6 +- .../organisation/TypeOrganisationTest.java | 16 -- 42 files changed, 2330 insertions(+), 58 deletions(-) create mode 100644 src/main/java/dev/lions/unionflow/server/api/dto/common/ErrorResponse.java create mode 100644 src/main/java/dev/lions/unionflow/server/api/dto/paiement/response/IntentionStatutResponse.java create mode 100644 src/main/java/dev/lions/unionflow/server/api/dto/souscription/FormuleAbonnementResponse.java create mode 100644 src/main/java/dev/lions/unionflow/server/api/dto/souscription/SouscriptionDemandeRequest.java create mode 100644 src/main/java/dev/lions/unionflow/server/api/dto/souscription/SouscriptionStatutResponse.java create mode 100644 src/main/java/dev/lions/unionflow/server/api/enums/abonnement/PlageMembres.java create mode 100644 src/main/java/dev/lions/unionflow/server/api/enums/abonnement/StatutValidationSouscription.java create mode 100644 src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypeOrganisationFacturation.java create mode 100644 src/test/java/dev/lions/unionflow/server/api/dto/membre/MembreSummaryResponseTest.java create mode 100644 src/test/java/dev/lions/unionflow/server/api/dto/paiement/response/IntentionStatutResponseTest.java create mode 100644 src/test/java/dev/lions/unionflow/server/api/dto/souscription/SouscriptionDtosTest.java create mode 100644 src/test/java/dev/lions/unionflow/server/api/enums/abonnement/PlageMembresTest.java create mode 100644 src/test/java/dev/lions/unionflow/server/api/enums/abonnement/StatutValidationSouscriptionTest.java create mode 100644 src/test/java/dev/lions/unionflow/server/api/enums/abonnement/TypeOrganisationFacturationTest.java diff --git a/src/main/java/dev/lions/unionflow/server/api/dto/common/ErrorResponse.java b/src/main/java/dev/lions/unionflow/server/api/dto/common/ErrorResponse.java new file mode 100644 index 0000000..6ea5870 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/api/dto/common/ErrorResponse.java @@ -0,0 +1,21 @@ +package dev.lions.unionflow.server.api.dto.common; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * DTO d'erreur unifié retourné par tous les endpoints REST. + * Remplace les ErrorResponse locales dupliquées dans chaque Resource. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ErrorResponse(String message, String error) { + + /** Constructeur pratique avec message uniquement (cas le plus courant). */ + public static ErrorResponse of(String message) { + return new ErrorResponse(message, null); + } + + /** Constructeur pratique avec les deux champs (compatibilité avec l'ancien format). */ + public static ErrorResponse ofError(String error) { + return new ErrorResponse(null, error); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/api/dto/cotisation/response/CotisationResponse.java b/src/main/java/dev/lions/unionflow/server/api/dto/cotisation/response/CotisationResponse.java index 4c45b0b..4e55c55 100644 --- a/src/main/java/dev/lions/unionflow/server/api/dto/cotisation/response/CotisationResponse.java +++ b/src/main/java/dev/lions/unionflow/server/api/dto/cotisation/response/CotisationResponse.java @@ -96,6 +96,32 @@ public class CotisationResponse extends BaseResponse { private Long joursRetard; private Boolean enRetard; + // === MÉTHODES DE FORMATAGE === + + public String getMontantDuFormatte() { + if (montantDu == null) return "0 FCFA"; + return String.format(java.util.Locale.US, "%,.0f %s", montantDu, codeDevise != null ? codeDevise : "FCFA"); + } + + public String getMontantPayeFormatte() { + if (montantPaye == null) return "0 FCFA"; + return String.format(java.util.Locale.US, "%,.0f %s", montantPaye, codeDevise != null ? codeDevise : "FCFA"); + } + + public String getMontantRestantFormatte() { + if (montantRestant == null) return "0 FCFA"; + return String.format(java.util.Locale.US, "%,.0f %s", montantRestant, codeDevise != null ? codeDevise : "FCFA"); + } + + public boolean isMontantRestantPositif() { + return montantRestant != null && montantRestant.signum() > 0; + } + + /** Alias de {@link #modePaiementLibelle} pour #{cotisation.methodePaiementLibelle}. */ + public String getMethodePaiementLibelle() { + return modePaiementLibelle; + } + // Informations de paiement private String methodePaiement; // WAVE_MONEY, VIREMENT, ESPECES, CARTE, MOBILE_MONEY private String referencePaiement; // Référence externe du paiement diff --git a/src/main/java/dev/lions/unionflow/server/api/dto/evenement/response/EvenementResponse.java b/src/main/java/dev/lions/unionflow/server/api/dto/evenement/response/EvenementResponse.java index 14ef9c9..c2cddb8 100644 --- a/src/main/java/dev/lions/unionflow/server/api/dto/evenement/response/EvenementResponse.java +++ b/src/main/java/dev/lions/unionflow/server/api/dto/evenement/response/EvenementResponse.java @@ -5,6 +5,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import dev.lions.unionflow.server.api.enums.evenement.PrioriteEvenement; import dev.lions.unionflow.server.api.enums.evenement.StatutEvenement; import dev.lions.unionflow.server.api.enums.evenement.TypeEvenementMetier; @@ -151,10 +152,82 @@ public class EvenementResponse extends BaseResponse { return typeEvenement != null ? typeEvenement.getLibelle() : "Non défini"; } + public String getTypeEvenementIcon() { + if (typeEvenement == null) return "pi pi-calendar"; + return switch (typeEvenement) { + case ASSEMBLEE_GENERALE -> "pi pi-building"; + case FORMATION -> "pi pi-book"; + case REUNION_BUREAU -> "pi pi-users"; + case CONFERENCE -> "pi pi-microphone"; + case ATELIER -> "pi pi-wrench"; + case CEREMONIE -> "pi pi-flag"; + case ACTIVITE_SOCIALE, ACTION_CARITATIVE, AUTRE -> "pi pi-calendar"; + }; + } + + public String getTypeEvenementSeverity() { + if (typeEvenement == null) return "secondary"; + return switch (typeEvenement) { + case ASSEMBLEE_GENERALE -> "warning"; + case FORMATION -> "info"; + case ACTIVITE_SOCIALE, ACTION_CARITATIVE, REUNION_BUREAU, CONFERENCE, ATELIER, CEREMONIE, AUTRE -> "secondary"; + }; + } + public String getStatutLibelle() { return statut != null ? statut.getLibelle() : "Non défini"; } + public String getStatutSeverity() { + if (statut == null) return "secondary"; + return switch (statut) { + case PLANIFIE -> "info"; + case EN_COURS -> "success"; + case TERMINE, CONFIRME -> "secondary"; + case ANNULE -> "danger"; + case REPORTE -> "warning"; + }; + } + + public String getDateDebutFormatee() { + return dateDebut != null ? dateDebut.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) : "—"; + } + + public String getHeureDebutFormatee() { + return heureDebut != null ? heureDebut.format(DateTimeFormatter.ofPattern("HH:mm")) : "—"; + } + + public String getHeureFinFormatee() { + return heureFin != null ? heureFin.format(DateTimeFormatter.ofPattern("HH:mm")) : "—"; + } + + public String getStatutIcon() { + if (statut == null) return "pi pi-question"; + return switch (statut) { + case PLANIFIE -> "pi pi-clock"; + case CONFIRME -> "pi pi-check-circle"; + case EN_COURS -> "pi pi-play"; + case TERMINE -> "pi pi-check"; + case ANNULE -> "pi pi-times"; + case REPORTE -> "pi pi-refresh"; + }; + } + + public String getPrioriteSeverity() { + if (priorite == null) return "secondary"; + return switch (priorite) { + case CRITIQUE -> "danger"; + case HAUTE -> "warning"; + case NORMALE -> "info"; + case BASSE -> "secondary"; + }; + } + + public String getBudgetFormate() { + if (budget == null) return "—"; + return String.format(java.util.Locale.US, "%,.0f %s", budget, codeDevise != null ? codeDevise : "FCFA"); + } + public String getPrioriteLibelle() { return priorite != null ? priorite.getLibelle() : "Normale"; } diff --git a/src/main/java/dev/lions/unionflow/server/api/dto/membre/request/CreateMembreRequest.java b/src/main/java/dev/lions/unionflow/server/api/dto/membre/request/CreateMembreRequest.java index 9482909..1a57671 100644 --- a/src/main/java/dev/lions/unionflow/server/api/dto/membre/request/CreateMembreRequest.java +++ b/src/main/java/dev/lions/unionflow/server/api/dto/membre/request/CreateMembreRequest.java @@ -40,7 +40,7 @@ public record CreateMembreRequest( @NotBlank @Size(max = 100) String nom, @NotBlank @Email @Size(max = 255) String email, @Size(max = 20) String telephone, - @Size(max = 13) String telephoneWave, + @Size(max = 20) String telephoneWave, @NotNull LocalDate dateNaissance, @Size(max = 100) String profession, @Size(max = 500) String photoUrl, diff --git a/src/main/java/dev/lions/unionflow/server/api/dto/membre/request/UpdateMembreRequest.java b/src/main/java/dev/lions/unionflow/server/api/dto/membre/request/UpdateMembreRequest.java index 8035c99..59f2c70 100644 --- a/src/main/java/dev/lions/unionflow/server/api/dto/membre/request/UpdateMembreRequest.java +++ b/src/main/java/dev/lions/unionflow/server/api/dto/membre/request/UpdateMembreRequest.java @@ -14,7 +14,7 @@ public record UpdateMembreRequest( @NotBlank @Size(max = 100) String nom, @NotBlank @Email @Size(max = 255) String email, @Size(max = 20) String telephone, - @Size(max = 13) String telephoneWave, + @Size(max = 20) String telephoneWave, @NotNull LocalDate dateNaissance, @Size(max = 100) String profession, @Size(max = 500) String photoUrl, diff --git a/src/main/java/dev/lions/unionflow/server/api/dto/membre/response/MembreResponse.java b/src/main/java/dev/lions/unionflow/server/api/dto/membre/response/MembreResponse.java index a55a99f..ae38058 100644 --- a/src/main/java/dev/lions/unionflow/server/api/dto/membre/response/MembreResponse.java +++ b/src/main/java/dev/lions/unionflow/server/api/dto/membre/response/MembreResponse.java @@ -2,6 +2,7 @@ package dev.lions.unionflow.server.api.dto.membre.response; import dev.lions.unionflow.server.api.dto.base.BaseResponse; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.UUID; import java.util.List; import lombok.AllArgsConstructor; @@ -65,6 +66,71 @@ public class MembreResponse extends BaseResponse { // ── Adhésion (contexte organisation) ─────── private UUID organisationId; - private String associationNom; + private String organisationNom; private LocalDate dateAdhesion; + + // ── Adresse principale ───────────────────── + private String adresse; + private String ville; + private String codePostal; + + // ── Notes / biographie ───────────────────── + private String notes; + + // ── Statistiques ─────────────────────────── + private int nombreEvenementsParticipes; + + // ── Provisionnement (retourné une seule fois à la création) ──── + /** Mot de passe temporaire généré lors du provisionnement Keycloak. + * Null pour toutes les autres opérations. L'utilisateur devra le changer à la première connexion. */ + private String motDePasseTemporaire; + + // ── Méthodes calculées pour la compatibilité JSF EL ──────────── + + /** Initiales (première lettre prénom + première lettre nom) pour les avatars JSF. */ + public String getInitiales() { + String p = (prenom != null && !prenom.isEmpty()) ? prenom.substring(0, 1).toUpperCase() : ""; + String n = (nom != null && !nom.isEmpty()) ? nom.substring(0, 1).toUpperCase() : ""; + return p + n; + } + + /** Date d'adhésion formatée "dd/MM/yyyy" pour #{membre.dateAdhesionFormatee}. */ + public String getDateAdhesionFormatee() { + if (dateAdhesion == null) return null; + return dateAdhesion.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")); + } + + /** Alias de statutCompte pour #{membre.statut}. */ + public String getStatut() { + return statutCompte; + } + + /** Alias de statutCompteSeverity pour #{membre.statutSeverity}. */ + public String getStatutSeverity() { + return statutCompteSeverity; + } + + /** Libellé du rôle principal calculé depuis les rôles. */ + public String getTypeMembre() { + if (roles == null || roles.isEmpty()) return "Membre"; + if (roles.contains("PRESIDENT")) return "Président"; + if (roles.contains("VICE_PRESIDENT")) return "Vice-Président"; + if (roles.contains("SECRETAIRE")) return "Secrétaire"; + if (roles.contains("TRESORIER")) return "Trésorier"; + if (roles.contains("ADMIN_ORGANISATION")) return "Administrateur"; + if (roles.contains("MODERATEUR")) return "Modérateur"; + return "Membre"; + } + + /** Severity PrimeUI calculée depuis les rôles. */ + public String getTypeSeverity() { + if (roles == null || roles.isEmpty()) return "secondary"; + if (roles.contains("PRESIDENT")) return "primary"; + if (roles.contains("VICE_PRESIDENT")) return "primary"; + if (roles.contains("SECRETAIRE")) return "info"; + if (roles.contains("TRESORIER")) return "warning"; + if (roles.contains("ADMIN_ORGANISATION")) return "danger"; + if (roles.contains("MODERATEUR")) return "warning"; + return "secondary"; + } } diff --git a/src/main/java/dev/lions/unionflow/server/api/dto/membre/response/MembreSummaryResponse.java b/src/main/java/dev/lions/unionflow/server/api/dto/membre/response/MembreSummaryResponse.java index 8a68bbc..1afc5df 100644 --- a/src/main/java/dev/lions/unionflow/server/api/dto/membre/response/MembreSummaryResponse.java +++ b/src/main/java/dev/lions/unionflow/server/api/dto/membre/response/MembreSummaryResponse.java @@ -1,10 +1,14 @@ package dev.lions.unionflow.server.api.dto.membre.response; -import java.util.UUID; +import java.time.LocalDate; import java.util.List; +import java.util.UUID; /** * DTO de réponse résumé pour Membre (listes et optimisations). + * + *
Ce record expose des méthodes calculées pour la compatibilité JSF EL :
+ * {@link #nomComplet()}, {@link #statut()}, {@link #typeMembre()}, etc.
*/
public record MembreSummaryResponse(
UUID id,
@@ -20,5 +24,107 @@ public record MembreSummaryResponse(
Boolean actif,
List Retournée par le endpoint {@code GET /api/souscriptions/formules} (PermitAll).
+ * Les prix affichés sont les prix de base avant application des coefficients
+ * de type d'organisation et de période.
+ *
+ * @author UnionFlow Team
+ * @version 1.0
+ * @since 2026-03-30
+ */
+public class FormuleAbonnementResponse {
+
+ /** Code de la formule : BASIC, STANDARD, PREMIUM */
+ private String code;
+
+ /** Libellé court de la formule */
+ private String libelle;
+
+ /** Description marketing de la formule */
+ private String description;
+
+ /** Code de la plage de membres : PETITE, MOYENNE, GRANDE, TRES_GRANDE */
+ private String plage;
+
+ /** Libellé de la plage (ex: "Petite structure (1–100 membres)") */
+ private String plageLibelle;
+
+ /** Nombre minimum de membres pour cette plage */
+ private int minMembres;
+
+ /**
+ * Nombre maximum de membres pour cette plage.
+ * -1 signifie illimité (TRES_GRANDE).
+ */
+ private int maxMembres;
+
+ /** Prix mensuel de base en XOF (sans remise de période ni coefficient) */
+ private BigDecimal prixMensuel;
+
+ /** Prix annuel équivalent (prixMensuel × 12 × coefficient annuel) */
+ private BigDecimal prixAnnuel;
+
+ /** Ordre d'affichage dans le catalogue */
+ private int ordreAffichage;
+
+ // ── Champs Option C ───────────────────────────────────────────────────────
+
+ /** Nom commercial du plan (MICRO, DECOUVERTE, ESSENTIEL, AVANCE, PROFESSIONNEL, ENTERPRISE) */
+ private String planCommercial;
+
+ /** Niveau de reporting (BASIQUE, STANDARD, AVANCE) */
+ private String niveauReporting;
+
+ /** Accès à l'API REST inclus */
+ private boolean apiAccess;
+
+ /** Module fédération multi-org inclus (ENTERPRISE) */
+ private boolean federationAccess;
+
+ /** Support prioritaire inclus */
+ private boolean supportPrioritaire;
+
+ /** SLA garanti (ex: "99.0%", "99.9%") */
+ private String slaGaranti;
+
+ /** Nombre maximum d'administrateurs (-1 = illimité) */
+ private int maxAdmins;
+
+ public FormuleAbonnementResponse() {}
+
+ // ─── Getters & Setters ───────────────────────────────────────────────────────
+
+ public String getCode() { return code; }
+ public void setCode(String code) { this.code = code; }
+
+ public String getLibelle() { return libelle; }
+ public void setLibelle(String libelle) { this.libelle = libelle; }
+
+ public String getDescription() { return description; }
+ public void setDescription(String description) { this.description = description; }
+
+ public String getPlage() { return plage; }
+ public void setPlage(String plage) { this.plage = plage; }
+
+ public String getPlageLibelle() { return plageLibelle; }
+ public void setPlageLibelle(String plageLibelle) { this.plageLibelle = plageLibelle; }
+
+ public int getMinMembres() { return minMembres; }
+ public void setMinMembres(int minMembres) { this.minMembres = minMembres; }
+
+ public int getMaxMembres() { return maxMembres; }
+ public void setMaxMembres(int maxMembres) { this.maxMembres = maxMembres; }
+
+ public BigDecimal getPrixMensuel() { return prixMensuel; }
+ public void setPrixMensuel(BigDecimal prixMensuel) { this.prixMensuel = prixMensuel; }
+
+ public BigDecimal getPrixAnnuel() { return prixAnnuel; }
+ public void setPrixAnnuel(BigDecimal prixAnnuel) { this.prixAnnuel = prixAnnuel; }
+
+ public int getOrdreAffichage() { return ordreAffichage; }
+ public void setOrdreAffichage(int ordreAffichage) { this.ordreAffichage = ordreAffichage; }
+
+ public String getPlanCommercial() { return planCommercial; }
+ public void setPlanCommercial(String planCommercial) { this.planCommercial = planCommercial; }
+
+ public String getNiveauReporting() { return niveauReporting; }
+ public void setNiveauReporting(String niveauReporting) { this.niveauReporting = niveauReporting; }
+
+ public boolean isApiAccess() { return apiAccess; }
+ public void setApiAccess(boolean apiAccess) { this.apiAccess = apiAccess; }
+
+ public boolean isFederationAccess() { return federationAccess; }
+ public void setFederationAccess(boolean federationAccess) { this.federationAccess = federationAccess; }
+
+ public boolean isSupportPrioritaire() { return supportPrioritaire; }
+ public void setSupportPrioritaire(boolean supportPrioritaire) { this.supportPrioritaire = supportPrioritaire; }
+
+ public String getSlaGaranti() { return slaGaranti; }
+ public void setSlaGaranti(String slaGaranti) { this.slaGaranti = slaGaranti; }
+
+ public int getMaxAdmins() { return maxAdmins; }
+ public void setMaxAdmins(int maxAdmins) { this.maxAdmins = maxAdmins; }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/api/dto/souscription/SouscriptionDemandeRequest.java b/src/main/java/dev/lions/unionflow/server/api/dto/souscription/SouscriptionDemandeRequest.java
new file mode 100644
index 0000000..2c80df3
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/api/dto/souscription/SouscriptionDemandeRequest.java
@@ -0,0 +1,79 @@
+package dev.lions.unionflow.server.api.dto.souscription;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+/**
+ * Requête de création d'une demande de souscription UnionFlow.
+ *
+ * Envoyée par l'ADMIN_ORGANISATION lors de l'onboarding pour sélectionner
+ * la formule, la plage de membres, la période et le type de son organisation.
+ *
+ * @author UnionFlow Team
+ * @version 1.0
+ * @since 2026-03-30
+ */
+public class SouscriptionDemandeRequest {
+
+ /** Niveau de formule : BASIC, STANDARD, PREMIUM */
+ @NotBlank(message = "Le type de formule est obligatoire")
+ private String typeFormule;
+
+ /** Tranche de membres : PETITE, MOYENNE, GRANDE, TRES_GRANDE */
+ @NotBlank(message = "La plage de membres est obligatoire")
+ private String plageMembres;
+
+ /** Périodicité de facturation : MENSUEL, TRIMESTRIEL, SEMESTRIEL, ANNUEL */
+ @NotBlank(message = "Le type de période est obligatoire")
+ private String typePeriode;
+
+ /** Type d'organisation pour le calcul du coefficient : ASSOCIATION, MUTUELLE, COOPERATIVE, FEDERATION
+ * Optionnel — si absent, le service le dérive depuis l'entité Organisation. */
+ private String typeOrganisation;
+
+ /** UUID de l'organisation concernée */
+ @NotNull(message = "L'identifiant de l'organisation est obligatoire")
+ private String organisationId;
+
+ public SouscriptionDemandeRequest() {}
+
+ public String getTypeFormule() {
+ return typeFormule;
+ }
+
+ public void setTypeFormule(String typeFormule) {
+ this.typeFormule = typeFormule;
+ }
+
+ public String getPlageMembres() {
+ return plageMembres;
+ }
+
+ public void setPlageMembres(String plageMembres) {
+ this.plageMembres = plageMembres;
+ }
+
+ public String getTypePeriode() {
+ return typePeriode;
+ }
+
+ public void setTypePeriode(String typePeriode) {
+ this.typePeriode = typePeriode;
+ }
+
+ public String getTypeOrganisation() {
+ return typeOrganisation;
+ }
+
+ public void setTypeOrganisation(String typeOrganisation) {
+ this.typeOrganisation = typeOrganisation;
+ }
+
+ public String getOrganisationId() {
+ return organisationId;
+ }
+
+ public void setOrganisationId(String organisationId) {
+ this.organisationId = organisationId;
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/api/dto/souscription/SouscriptionStatutResponse.java b/src/main/java/dev/lions/unionflow/server/api/dto/souscription/SouscriptionStatutResponse.java
new file mode 100644
index 0000000..d91a9e2
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/api/dto/souscription/SouscriptionStatutResponse.java
@@ -0,0 +1,155 @@
+package dev.lions.unionflow.server.api.dto.souscription;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+/**
+ * Réponse décrivant l'état courant d'une souscription UnionFlow.
+ *
+ * Retournée par les endpoints de création de demande, de consultation
+ * et d'initiation du paiement Wave.
+ *
+ * @author UnionFlow Team
+ * @version 1.0
+ * @since 2026-03-30
+ */
+public class SouscriptionStatutResponse {
+
+ private String souscriptionId;
+ private String statutValidation;
+ private String statutLibelle;
+ private String typeFormule;
+ private String plageMembres;
+ private String plageLibelle;
+ private String typePeriode;
+ private String typeOrganisation;
+ private BigDecimal montantTotal;
+ private BigDecimal montantMensuelBase;
+ private BigDecimal coefficientApplique;
+ private String waveSessionId;
+ /** URL Wave à ouvrir dans le navigateur/WebView pour initier le paiement. */
+ private String waveLaunchUrl;
+ private LocalDate dateDebut;
+ private LocalDate dateFin;
+ private LocalDate dateValidation;
+ private String commentaireRejet;
+ private String organisationId;
+ private String organisationNom;
+
+ // ── Quota & features (Option C) ─────────────────────────────────────────
+ private Integer quotaMax;
+ private Integer quotaUtilise;
+ private Integer quotaRestant;
+ private boolean quotaDepasse;
+ private String planCommercial;
+ private boolean apiAccess;
+ private boolean federationAccess;
+ private boolean supportPrioritaire;
+ private String slaGaranti;
+ private Integer maxAdmins;
+ private String niveauReporting;
+ /** Nombre de jours avant expiration (négatif = déjà expiré) */
+ private long joursAvantExpiration;
+ /** Statut de la souscription : ACTIVE, EXPIREE, SUSPENDUE, EN_ATTENTE, RESILIEE */
+ private String statut;
+
+ public SouscriptionStatutResponse() {}
+
+ // ─── Getters & Setters ───────────────────────────────────────────────────────
+
+ public String getSouscriptionId() { return souscriptionId; }
+ public void setSouscriptionId(String souscriptionId) { this.souscriptionId = souscriptionId; }
+
+ public String getStatutValidation() { return statutValidation; }
+ public void setStatutValidation(String statutValidation) { this.statutValidation = statutValidation; }
+
+ public String getStatutLibelle() { return statutLibelle; }
+ public void setStatutLibelle(String statutLibelle) { this.statutLibelle = statutLibelle; }
+
+ public String getTypeFormule() { return typeFormule; }
+ public void setTypeFormule(String typeFormule) { this.typeFormule = typeFormule; }
+
+ public String getPlageMembres() { return plageMembres; }
+ public void setPlageMembres(String plageMembres) { this.plageMembres = plageMembres; }
+
+ public String getPlageLibelle() { return plageLibelle; }
+ public void setPlageLibelle(String plageLibelle) { this.plageLibelle = plageLibelle; }
+
+ public String getTypePeriode() { return typePeriode; }
+ public void setTypePeriode(String typePeriode) { this.typePeriode = typePeriode; }
+
+ public String getTypeOrganisation() { return typeOrganisation; }
+ public void setTypeOrganisation(String typeOrganisation) { this.typeOrganisation = typeOrganisation; }
+
+ public BigDecimal getMontantTotal() { return montantTotal; }
+ public void setMontantTotal(BigDecimal montantTotal) { this.montantTotal = montantTotal; }
+
+ public BigDecimal getMontantMensuelBase() { return montantMensuelBase; }
+ public void setMontantMensuelBase(BigDecimal montantMensuelBase) { this.montantMensuelBase = montantMensuelBase; }
+
+ public BigDecimal getCoefficientApplique() { return coefficientApplique; }
+ public void setCoefficientApplique(BigDecimal coefficientApplique) { this.coefficientApplique = coefficientApplique; }
+
+ public String getWaveSessionId() { return waveSessionId; }
+ public void setWaveSessionId(String waveSessionId) { this.waveSessionId = waveSessionId; }
+
+ public String getWaveLaunchUrl() { return waveLaunchUrl; }
+ public void setWaveLaunchUrl(String waveLaunchUrl) { this.waveLaunchUrl = waveLaunchUrl; }
+
+ public LocalDate getDateDebut() { return dateDebut; }
+ public void setDateDebut(LocalDate dateDebut) { this.dateDebut = dateDebut; }
+
+ public LocalDate getDateFin() { return dateFin; }
+ public void setDateFin(LocalDate dateFin) { this.dateFin = dateFin; }
+
+ public LocalDate getDateValidation() { return dateValidation; }
+ public void setDateValidation(LocalDate dateValidation) { this.dateValidation = dateValidation; }
+
+ public String getCommentaireRejet() { return commentaireRejet; }
+ public void setCommentaireRejet(String commentaireRejet) { this.commentaireRejet = commentaireRejet; }
+
+ public String getOrganisationId() { return organisationId; }
+ public void setOrganisationId(String organisationId) { this.organisationId = organisationId; }
+
+ public String getOrganisationNom() { return organisationNom; }
+ public void setOrganisationNom(String organisationNom) { this.organisationNom = organisationNom; }
+
+ public Integer getQuotaMax() { return quotaMax; }
+ public void setQuotaMax(Integer quotaMax) { this.quotaMax = quotaMax; }
+
+ public Integer getQuotaUtilise() { return quotaUtilise; }
+ public void setQuotaUtilise(Integer quotaUtilise) { this.quotaUtilise = quotaUtilise; }
+
+ public Integer getQuotaRestant() { return quotaRestant; }
+ public void setQuotaRestant(Integer quotaRestant) { this.quotaRestant = quotaRestant; }
+
+ public boolean isQuotaDepasse() { return quotaDepasse; }
+ public void setQuotaDepasse(boolean quotaDepasse) { this.quotaDepasse = quotaDepasse; }
+
+ public String getPlanCommercial() { return planCommercial; }
+ public void setPlanCommercial(String planCommercial) { this.planCommercial = planCommercial; }
+
+ public boolean isApiAccess() { return apiAccess; }
+ public void setApiAccess(boolean apiAccess) { this.apiAccess = apiAccess; }
+
+ public boolean isFederationAccess() { return federationAccess; }
+ public void setFederationAccess(boolean federationAccess) { this.federationAccess = federationAccess; }
+
+ public boolean isSupportPrioritaire() { return supportPrioritaire; }
+ public void setSupportPrioritaire(boolean supportPrioritaire) { this.supportPrioritaire = supportPrioritaire; }
+
+ public String getSlaGaranti() { return slaGaranti; }
+ public void setSlaGaranti(String slaGaranti) { this.slaGaranti = slaGaranti; }
+
+ public Integer getMaxAdmins() { return maxAdmins; }
+ public void setMaxAdmins(Integer maxAdmins) { this.maxAdmins = maxAdmins; }
+
+ public String getNiveauReporting() { return niveauReporting; }
+ public void setNiveauReporting(String niveauReporting) { this.niveauReporting = niveauReporting; }
+
+ public long getJoursAvantExpiration() { return joursAvantExpiration; }
+ public void setJoursAvantExpiration(long joursAvantExpiration) { this.joursAvantExpiration = joursAvantExpiration; }
+
+ public String getStatut() { return statut; }
+ public void setStatut(String statut) { this.statut = statut; }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/PlageMembres.java b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/PlageMembres.java
new file mode 100644
index 0000000..c95bdbe
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/PlageMembres.java
@@ -0,0 +1,64 @@
+package dev.lions.unionflow.server.api.enums.abonnement;
+
+/**
+ * Tranches de taille d'organisation pour le calcul tarifaire UnionFlow.
+ *
+ * Chaque plage définit un intervalle [min, max] de membres.
+ * La plage TRES_GRANDE est sans limite supérieure (Integer.MAX_VALUE).
+ *
+ * @author UnionFlow Team
+ * @version 1.0
+ * @since 2026-03-30
+ */
+public enum PlageMembres {
+
+ PETITE("Petite structure (1–100 membres)", 1, 100),
+ MOYENNE("Moyenne structure (101–500 membres)", 101, 500),
+ GRANDE("Grande structure (501–2 000 membres)", 501, 2000),
+ TRES_GRANDE("Très grande structure (2 000+ membres)", 2001, Integer.MAX_VALUE);
+
+ private final String libelle;
+ private final int min;
+ private final int max;
+
+ PlageMembres(String libelle, int min, int max) {
+ this.libelle = libelle;
+ this.min = min;
+ this.max = max;
+ }
+
+ /**
+ * Détermine la plage tarifaire correspondant à un nombre de membres donné.
+ *
+ * @param nombre nombre de membres de l'organisation
+ * @return la plage correspondante (TRES_GRANDE si aucune ne correspond)
+ */
+ public static PlageMembres fromNombreMembres(int nombre) {
+ for (PlageMembres p : values()) {
+ if (nombre >= p.min && nombre <= p.max) {
+ return p;
+ }
+ }
+ return TRES_GRANDE;
+ }
+
+ public String getLibelle() {
+ return libelle;
+ }
+
+ public int getMin() {
+ return min;
+ }
+
+ /**
+ * Retourne le max de membres pour la plage.
+ * Retourne -1 pour TRES_GRANDE (illimité) afin de faciliter la sérialisation JSON.
+ */
+ public int getMaxAffichage() {
+ return this == TRES_GRANDE ? -1 : max;
+ }
+
+ public int getMax() {
+ return max;
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/StatutSouscription.java b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/StatutSouscription.java
index c2a1976..cb9ba96 100644
--- a/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/StatutSouscription.java
+++ b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/StatutSouscription.java
@@ -1,6 +1,7 @@
package dev.lions.unionflow.server.api.enums.abonnement;
public enum StatutSouscription {
+ EN_ATTENTE("En attente de validation"),
ACTIVE("Active"),
EXPIREE("Expirée"),
SUSPENDUE("Suspendue — quota dépassé ou impayé"),
diff --git a/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/StatutValidationSouscription.java b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/StatutValidationSouscription.java
new file mode 100644
index 0000000..02e0b85
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/StatutValidationSouscription.java
@@ -0,0 +1,43 @@
+package dev.lions.unionflow.server.api.enums.abonnement;
+
+/**
+ * Cycle de vie de validation d'une souscription UnionFlow.
+ *
+ * Transitions valides :
+ * Trois niveaux disponibles, chacun applicable à toutes les plages de taille
+ * ({@link PlageMembres}). Les prix de base sont définis dans la matrice tarifaire
+ * et varient selon la plage et le type d'organisation.
+ *
+ * Migration depuis l'ancien enum : STARTER→BASIC, CRYSTAL supprimé.
*
* @author UnionFlow Team
- * @version 1.0
- * @since 2025-01-10
+ * @version 2.0
+ * @since 2026-03-30
*/
public enum TypeFormule {
- STARTER("Starter — 1 à 50 membres — 5 000 XOF/mois"),
- STANDARD("Standard — 51 à 200 membres — 7 000 XOF/mois"),
- PREMIUM("Premium — 201 à 500 membres — 9 000 XOF/mois"),
- CRYSTAL("Crystal — 501+ membres — 10 000 XOF/mois");
+
+ BASIC("Basic"),
+ STANDARD("Standard"),
+ PREMIUM("Premium");
private final String libelle;
diff --git a/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypeOrganisationFacturation.java b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypeOrganisationFacturation.java
new file mode 100644
index 0000000..623cce0
--- /dev/null
+++ b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypeOrganisationFacturation.java
@@ -0,0 +1,51 @@
+package dev.lions.unionflow.server.api.enums.abonnement;
+
+import java.math.BigDecimal;
+
+/**
+ * Catégorie tarifaire d'une organisation pour le calcul du prix de souscription.
+ *
+ * Distinct de {@code TypeOrganisation} (catégorisation métier).
+ * Chaque valeur porte deux coefficients multiplicateurs :
+ * Exemple : FEDERATION paye le tarif normal (×1.0) en BASIC/STANDARD
+ * mais un coefficient ×1.5 en PREMIUM (fonctionnalités multi-entités).
+ */
+public enum TypeOrganisationFacturation {
+
+ ASSOCIATION("Association / ONG locale", new BigDecimal("1.0"), new BigDecimal("1.0")),
+ MUTUELLE("Mutuelle (santé, fonctionnaires, scolaires)", new BigDecimal("1.2"), new BigDecimal("1.2")),
+ COOPERATIVE("Coopérative / Microfinance", new BigDecimal("1.3"), new BigDecimal("1.3")),
+ FEDERATION("Fédération / Grande ONG", new BigDecimal("1.0"), new BigDecimal("1.5"));
+
+ private final String libelle;
+ private final BigDecimal coefficientBase;
+ private final BigDecimal coefficientPremium;
+
+ TypeOrganisationFacturation(String libelle, BigDecimal coefficientBase, BigDecimal coefficientPremium) {
+ this.libelle = libelle;
+ this.coefficientBase = coefficientBase;
+ this.coefficientPremium = coefficientPremium;
+ }
+
+ /**
+ * Retourne le coefficient tarifaire approprié selon le code de formule.
+ *
+ * @param typeFormule code de la formule (BASIC, STANDARD, PREMIUM)
+ * @return coefficient multiplicateur du prix mensuel de base
+ */
+ public BigDecimal getCoefficient(String typeFormule) {
+ if ("PREMIUM".equals(typeFormule) && this == FEDERATION) {
+ return coefficientPremium;
+ }
+ return coefficientBase;
+ }
+
+ public String getLibelle() { return libelle; }
+ public BigDecimal getCoefficientBase() { return coefficientBase; }
+ public BigDecimal getCoefficientPremium() { return coefficientPremium; }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypePeriodeAbonnement.java b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypePeriodeAbonnement.java
index 3258154..f683b1b 100644
--- a/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypePeriodeAbonnement.java
+++ b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypePeriodeAbonnement.java
@@ -1,12 +1,50 @@
package dev.lions.unionflow.server.api.enums.abonnement;
+import java.math.BigDecimal;
+
+/**
+ * Périodes d'abonnement UnionFlow avec remises associées.
+ *
+ * Le {@code coefficient} est multiplié par le prix mensuel de base,
+ * puis par le nombre de mois pour obtenir le montant total.
+ *
+ * Utilisé pour la catégorisation métier des organisations
+ * (Lions Club, Mutuelle santé, ONG, etc.). Ne pas confondre avec
+ * {@link TypeOrganisationFacturation} qui porte les coefficients tarifaires.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-10
*/
public enum TypeOrganisation {
+
ASSOCIATION("Association"),
MUTUELLE_EPARGNE_CREDIT("Mutuelle d'épargne et de crédit"),
MUTUELLE_SANTE("Mutuelle de santé"),
TONTINE("Tontine / épargne rotative"),
ONG("ONG / Association humanitaire"),
- COOPERATIVE_AGRICOLE("Coopérative agricole / production"),
- ASSOCIATION_PROFESSIONNELLE("Association professionnelle / Ordre"),
- ASSOCIATION_COMMUNAUTAIRE("Association communautaire / quartier"),
+ COOPERATIVE_AGRICOLE("Coopérative agricole"),
+ ASSOCIATION_PROFESSIONNELLE("Association professionnelle"),
+ ASSOCIATION_COMMUNAUTAIRE("Association communautaire"),
ORGANISATION_RELIGIEUSE("Organisation religieuse"),
FEDERATION("Fédération / Union d'associations"),
SYNDICAT("Syndicat non partisan"),
diff --git a/src/test/java/dev/lions/unionflow/server/api/TestDataFactory.java b/src/test/java/dev/lions/unionflow/server/api/TestDataFactory.java
index 2ef9c6a..3433bd2 100644
--- a/src/test/java/dev/lions/unionflow/server/api/TestDataFactory.java
+++ b/src/test/java/dev/lions/unionflow/server/api/TestDataFactory.java
@@ -40,7 +40,7 @@ public final class TestDataFactory {
String email) {
return new MembreSummaryResponse(
UUID.randomUUID(), numero, prenom, nom, email, "0102030405", "Profession", "ACTIF", "Actif", "success", true,
- List.of("MEMBRE"), null, null);
+ List.of("MEMBRE"), null, null, null);
}
public static CreateMembreRequest createCreateMembreRequest(int age) {
diff --git a/src/test/java/dev/lions/unionflow/server/api/dto/cotisation/response/CotisationResponseTest.java b/src/test/java/dev/lions/unionflow/server/api/dto/cotisation/response/CotisationResponseTest.java
index 953a1c9..7d8c441 100644
--- a/src/test/java/dev/lions/unionflow/server/api/dto/cotisation/response/CotisationResponseTest.java
+++ b/src/test/java/dev/lions/unionflow/server/api/dto/cotisation/response/CotisationResponseTest.java
@@ -132,6 +132,102 @@ class CotisationResponseTest {
}
}
+ @Nested
+ @DisplayName("Méthodes de formatage des montants")
+ class MethodesFormatageMontants {
+
+ @Test
+ @DisplayName("getMontantDuFormatte retourne 0 FCFA quand montantDu null")
+ void testMontantDuNull() {
+ CotisationResponse r = CotisationResponse.builder().build();
+ assertThat(r.getMontantDuFormatte()).isEqualTo("0 FCFA");
+ }
+
+ @Test
+ @DisplayName("getMontantDuFormatte formate avec devise par défaut FCFA")
+ void testMontantDuSansDevise() {
+ CotisationResponse r = CotisationResponse.builder().montantDu(BigDecimal.valueOf(5000)).build();
+ assertThat(r.getMontantDuFormatte()).isEqualTo("5,000 FCFA");
+ }
+
+ @Test
+ @DisplayName("getMontantDuFormatte formate avec codeDevise fourni")
+ void testMontantDuAvecDevise() {
+ CotisationResponse r = CotisationResponse.builder().montantDu(BigDecimal.valueOf(5000)).codeDevise("XOF").build();
+ assertThat(r.getMontantDuFormatte()).isEqualTo("5,000 XOF");
+ }
+
+ @Test
+ @DisplayName("getMontantPayeFormatte retourne 0 FCFA quand montantPaye null")
+ void testMontantPayeNull() {
+ CotisationResponse r = CotisationResponse.builder().build();
+ assertThat(r.getMontantPayeFormatte()).isEqualTo("0 FCFA");
+ }
+
+ @Test
+ @DisplayName("getMontantPayeFormatte formate avec devise par défaut FCFA")
+ void testMontantPayeSansDevise() {
+ CotisationResponse r = CotisationResponse.builder().montantPaye(BigDecimal.valueOf(2000)).build();
+ assertThat(r.getMontantPayeFormatte()).isEqualTo("2,000 FCFA");
+ }
+
+ @Test
+ @DisplayName("getMontantPayeFormatte formate avec codeDevise fourni")
+ void testMontantPayeAvecDevise() {
+ CotisationResponse r = CotisationResponse.builder().montantPaye(BigDecimal.valueOf(2000)).codeDevise("EUR").build();
+ assertThat(r.getMontantPayeFormatte()).isEqualTo("2,000 EUR");
+ }
+
+ @Test
+ @DisplayName("getMontantRestantFormatte retourne 0 FCFA quand montantRestant null")
+ void testMontantRestantNull() {
+ CotisationResponse r = CotisationResponse.builder().build();
+ assertThat(r.getMontantRestantFormatte()).isEqualTo("0 FCFA");
+ }
+
+ @Test
+ @DisplayName("getMontantRestantFormatte formate avec devise par défaut FCFA")
+ void testMontantRestantSansDevise() {
+ CotisationResponse r = CotisationResponse.builder().montantRestant(BigDecimal.valueOf(3000)).build();
+ assertThat(r.getMontantRestantFormatte()).isEqualTo("3,000 FCFA");
+ }
+
+ @Test
+ @DisplayName("getMontantRestantFormatte formate avec codeDevise fourni")
+ void testMontantRestantAvecDevise() {
+ CotisationResponse r = CotisationResponse.builder().montantRestant(BigDecimal.valueOf(3000)).codeDevise("XOF").build();
+ assertThat(r.getMontantRestantFormatte()).isEqualTo("3,000 XOF");
+ }
+
+ @Test
+ @DisplayName("isMontantRestantPositif retourne false quand montantRestant null")
+ void testMontantRestantPositifNull() {
+ CotisationResponse r = CotisationResponse.builder().build();
+ assertThat(r.isMontantRestantPositif()).isFalse();
+ }
+
+ @Test
+ @DisplayName("isMontantRestantPositif retourne false quand montantRestant = 0")
+ void testMontantRestantPositifZero() {
+ CotisationResponse r = CotisationResponse.builder().montantRestant(BigDecimal.ZERO).build();
+ assertThat(r.isMontantRestantPositif()).isFalse();
+ }
+
+ @Test
+ @DisplayName("isMontantRestantPositif retourne false quand montantRestant négatif")
+ void testMontantRestantPositifNegatif() {
+ CotisationResponse r = CotisationResponse.builder().montantRestant(BigDecimal.valueOf(-100)).build();
+ assertThat(r.isMontantRestantPositif()).isFalse();
+ }
+
+ @Test
+ @DisplayName("isMontantRestantPositif retourne true quand montantRestant > 0")
+ void testMontantRestantPositifVrai() {
+ CotisationResponse r = CotisationResponse.builder().montantRestant(BigDecimal.valueOf(500)).build();
+ assertThat(r.isMontantRestantPositif()).isTrue();
+ }
+ }
+
@Nested
@DisplayName("Builder complet")
class BuilderComplet {
diff --git a/src/test/java/dev/lions/unionflow/server/api/dto/evenement/response/EvenementResponseTest.java b/src/test/java/dev/lions/unionflow/server/api/dto/evenement/response/EvenementResponseTest.java
index 0a890c8..9264da4 100644
--- a/src/test/java/dev/lions/unionflow/server/api/dto/evenement/response/EvenementResponseTest.java
+++ b/src/test/java/dev/lions/unionflow/server/api/dto/evenement/response/EvenementResponseTest.java
@@ -492,4 +492,259 @@ class EvenementResponseTest {
assertThat(r.estComplet()).isFalse();
}
}
+
+ @Nested
+ @DisplayName("getTypeEvenementIcon")
+ class GetTypeEvenementIcon {
+
+ @Test
+ @DisplayName("retourne icône par défaut quand type null")
+ void testNull() {
+ EvenementResponse r = EvenementResponse.builder().build();
+ assertThat(r.getTypeEvenementIcon()).isEqualTo("pi pi-calendar");
+ }
+
+ @Test
+ @DisplayName("ASSEMBLEE_GENERALE → pi pi-building")
+ void testAssembleeGenerale() {
+ EvenementResponse r = EvenementResponse.builder().typeEvenement(TypeEvenementMetier.ASSEMBLEE_GENERALE).build();
+ assertThat(r.getTypeEvenementIcon()).isEqualTo("pi pi-building");
+ }
+
+ @Test
+ @DisplayName("FORMATION → pi pi-book")
+ void testFormation() {
+ EvenementResponse r = EvenementResponse.builder().typeEvenement(TypeEvenementMetier.FORMATION).build();
+ assertThat(r.getTypeEvenementIcon()).isEqualTo("pi pi-book");
+ }
+
+ @Test
+ @DisplayName("REUNION_BUREAU → pi pi-users")
+ void testReunionBureau() {
+ EvenementResponse r = EvenementResponse.builder().typeEvenement(TypeEvenementMetier.REUNION_BUREAU).build();
+ assertThat(r.getTypeEvenementIcon()).isEqualTo("pi pi-users");
+ }
+
+ @Test
+ @DisplayName("CONFERENCE → pi pi-microphone")
+ void testConference() {
+ EvenementResponse r = EvenementResponse.builder().typeEvenement(TypeEvenementMetier.CONFERENCE).build();
+ assertThat(r.getTypeEvenementIcon()).isEqualTo("pi pi-microphone");
+ }
+
+ @Test
+ @DisplayName("ATELIER → pi pi-wrench")
+ void testAtelier() {
+ EvenementResponse r = EvenementResponse.builder().typeEvenement(TypeEvenementMetier.ATELIER).build();
+ assertThat(r.getTypeEvenementIcon()).isEqualTo("pi pi-wrench");
+ }
+
+ @Test
+ @DisplayName("CEREMONIE → pi pi-flag")
+ void testCeremonie() {
+ EvenementResponse r = EvenementResponse.builder().typeEvenement(TypeEvenementMetier.CEREMONIE).build();
+ assertThat(r.getTypeEvenementIcon()).isEqualTo("pi pi-flag");
+ }
+
+ @Test
+ @DisplayName("ACTIVITE_SOCIALE, ACTION_CARITATIVE, AUTRE → pi pi-calendar")
+ void testAutres() {
+ assertThat(EvenementResponse.builder().typeEvenement(TypeEvenementMetier.ACTIVITE_SOCIALE).build().getTypeEvenementIcon()).isEqualTo("pi pi-calendar");
+ assertThat(EvenementResponse.builder().typeEvenement(TypeEvenementMetier.ACTION_CARITATIVE).build().getTypeEvenementIcon()).isEqualTo("pi pi-calendar");
+ assertThat(EvenementResponse.builder().typeEvenement(TypeEvenementMetier.AUTRE).build().getTypeEvenementIcon()).isEqualTo("pi pi-calendar");
+ }
+ }
+
+ @Nested
+ @DisplayName("getTypeEvenementSeverity")
+ class GetTypeEvenementSeverity {
+
+ @Test
+ @DisplayName("retourne secondary quand type null")
+ void testNull() {
+ EvenementResponse r = EvenementResponse.builder().build();
+ assertThat(r.getTypeEvenementSeverity()).isEqualTo("secondary");
+ }
+
+ @Test
+ @DisplayName("ASSEMBLEE_GENERALE → warning")
+ void testAssembleeGenerale() {
+ EvenementResponse r = EvenementResponse.builder().typeEvenement(TypeEvenementMetier.ASSEMBLEE_GENERALE).build();
+ assertThat(r.getTypeEvenementSeverity()).isEqualTo("warning");
+ }
+
+ @Test
+ @DisplayName("FORMATION → info")
+ void testFormation() {
+ EvenementResponse r = EvenementResponse.builder().typeEvenement(TypeEvenementMetier.FORMATION).build();
+ assertThat(r.getTypeEvenementSeverity()).isEqualTo("info");
+ }
+
+ @Test
+ @DisplayName("autres types → secondary")
+ void testAutres() {
+ for (TypeEvenementMetier t : new TypeEvenementMetier[]{
+ TypeEvenementMetier.ACTIVITE_SOCIALE, TypeEvenementMetier.ACTION_CARITATIVE,
+ TypeEvenementMetier.REUNION_BUREAU, TypeEvenementMetier.CONFERENCE,
+ TypeEvenementMetier.ATELIER, TypeEvenementMetier.CEREMONIE, TypeEvenementMetier.AUTRE}) {
+ assertThat(EvenementResponse.builder().typeEvenement(t).build().getTypeEvenementSeverity())
+ .as("Type: %s", t).isEqualTo("secondary");
+ }
+ }
+ }
+
+ @Nested
+ @DisplayName("getStatutSeverity")
+ class GetStatutSeverity {
+
+ @Test
+ @DisplayName("retourne secondary quand statut null")
+ void testNull() {
+ EvenementResponse r = EvenementResponse.builder().build();
+ assertThat(r.getStatutSeverity()).isEqualTo("secondary");
+ }
+
+ @Test
+ @DisplayName("PLANIFIE → info")
+ void testPlanifie() {
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.PLANIFIE).build().getStatutSeverity()).isEqualTo("info");
+ }
+
+ @Test
+ @DisplayName("EN_COURS → success")
+ void testEnCours() {
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.EN_COURS).build().getStatutSeverity()).isEqualTo("success");
+ }
+
+ @Test
+ @DisplayName("TERMINE et CONFIRME → secondary")
+ void testTermineConfirme() {
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.TERMINE).build().getStatutSeverity()).isEqualTo("secondary");
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.CONFIRME).build().getStatutSeverity()).isEqualTo("secondary");
+ }
+
+ @Test
+ @DisplayName("ANNULE → danger")
+ void testAnnule() {
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.ANNULE).build().getStatutSeverity()).isEqualTo("danger");
+ }
+
+ @Test
+ @DisplayName("REPORTE → warning")
+ void testReporte() {
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.REPORTE).build().getStatutSeverity()).isEqualTo("warning");
+ }
+ }
+
+ @Nested
+ @DisplayName("getStatutIcon")
+ class GetStatutIcon {
+
+ @Test
+ @DisplayName("retourne pi pi-question quand statut null")
+ void testNull() {
+ EvenementResponse r = EvenementResponse.builder().build();
+ assertThat(r.getStatutIcon()).isEqualTo("pi pi-question");
+ }
+
+ @Test
+ @DisplayName("tous les statuts retournent la bonne icône")
+ void testTousStatuts() {
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.PLANIFIE).build().getStatutIcon()).isEqualTo("pi pi-clock");
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.CONFIRME).build().getStatutIcon()).isEqualTo("pi pi-check-circle");
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.EN_COURS).build().getStatutIcon()).isEqualTo("pi pi-play");
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.TERMINE).build().getStatutIcon()).isEqualTo("pi pi-check");
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.ANNULE).build().getStatutIcon()).isEqualTo("pi pi-times");
+ assertThat(EvenementResponse.builder().statut(StatutEvenement.REPORTE).build().getStatutIcon()).isEqualTo("pi pi-refresh");
+ }
+ }
+
+ @Nested
+ @DisplayName("getPrioriteSeverity")
+ class GetPrioriteSeverity {
+
+ @Test
+ @DisplayName("retourne secondary quand priorite null")
+ void testNull() {
+ EvenementResponse r = EvenementResponse.builder().build();
+ assertThat(r.getPrioriteSeverity()).isEqualTo("secondary");
+ }
+
+ @Test
+ @DisplayName("toutes les priorités retournent la bonne sévérité")
+ void testToutesPriorites() {
+ assertThat(EvenementResponse.builder().priorite(PrioriteEvenement.CRITIQUE).build().getPrioriteSeverity()).isEqualTo("danger");
+ assertThat(EvenementResponse.builder().priorite(PrioriteEvenement.HAUTE).build().getPrioriteSeverity()).isEqualTo("warning");
+ assertThat(EvenementResponse.builder().priorite(PrioriteEvenement.NORMALE).build().getPrioriteSeverity()).isEqualTo("info");
+ assertThat(EvenementResponse.builder().priorite(PrioriteEvenement.BASSE).build().getPrioriteSeverity()).isEqualTo("secondary");
+ }
+ }
+
+ @Nested
+ @DisplayName("getDateDebutFormatee, getHeureDebutFormatee, getHeureFinFormatee, getBudgetFormate")
+ class MethodesFormatage {
+
+ @Test
+ @DisplayName("getDateDebutFormatee retourne — quand null")
+ void testDateDebutNull() {
+ EvenementResponse r = EvenementResponse.builder().build();
+ assertThat(r.getDateDebutFormatee()).isEqualTo("—");
+ }
+
+ @Test
+ @DisplayName("getDateDebutFormatee formate en dd/MM/yyyy")
+ void testDateDebut() {
+ EvenementResponse r = EvenementResponse.builder().dateDebut(java.time.LocalDate.of(2026, 4, 7)).build();
+ assertThat(r.getDateDebutFormatee()).isEqualTo("07/04/2026");
+ }
+
+ @Test
+ @DisplayName("getHeureDebutFormatee retourne — quand null")
+ void testHeureDebutNull() {
+ EvenementResponse r = EvenementResponse.builder().build();
+ assertThat(r.getHeureDebutFormatee()).isEqualTo("—");
+ }
+
+ @Test
+ @DisplayName("getHeureDebutFormatee formate en HH:mm")
+ void testHeureDebut() {
+ EvenementResponse r = EvenementResponse.builder().heureDebut(java.time.LocalTime.of(9, 30)).build();
+ assertThat(r.getHeureDebutFormatee()).isEqualTo("09:30");
+ }
+
+ @Test
+ @DisplayName("getHeureFinFormatee retourne — quand null")
+ void testHeureFinNull() {
+ EvenementResponse r = EvenementResponse.builder().build();
+ assertThat(r.getHeureFinFormatee()).isEqualTo("—");
+ }
+
+ @Test
+ @DisplayName("getHeureFinFormatee formate en HH:mm")
+ void testHeureFin() {
+ EvenementResponse r = EvenementResponse.builder().heureFin(java.time.LocalTime.of(18, 0)).build();
+ assertThat(r.getHeureFinFormatee()).isEqualTo("18:00");
+ }
+
+ @Test
+ @DisplayName("getBudgetFormate retourne — quand budget null")
+ void testBudgetNull() {
+ EvenementResponse r = EvenementResponse.builder().build();
+ assertThat(r.getBudgetFormate()).isEqualTo("—");
+ }
+
+ @Test
+ @DisplayName("getBudgetFormate formate avec devise par défaut FCFA")
+ void testBudgetSansDevise() {
+ EvenementResponse r = EvenementResponse.builder().budget(java.math.BigDecimal.valueOf(50000)).build();
+ assertThat(r.getBudgetFormate()).isEqualTo("50,000 FCFA");
+ }
+
+ @Test
+ @DisplayName("getBudgetFormate formate avec codeDevise fourni")
+ void testBudgetAvecDevise() {
+ EvenementResponse r = EvenementResponse.builder().budget(java.math.BigDecimal.valueOf(50000)).codeDevise("EUR").build();
+ assertThat(r.getBudgetFormate()).isEqualTo("50,000 EUR");
+ }
+ }
}
diff --git a/src/test/java/dev/lions/unionflow/server/api/dto/membre/MembreSearchResultDTOTest.java b/src/test/java/dev/lions/unionflow/server/api/dto/membre/MembreSearchResultDTOTest.java
index b088c83..4c6a98d 100644
--- a/src/test/java/dev/lions/unionflow/server/api/dto/membre/MembreSearchResultDTOTest.java
+++ b/src/test/java/dev/lions/unionflow/server/api/dto/membre/MembreSearchResultDTOTest.java
@@ -23,7 +23,7 @@ class MembreSearchResultDTOTest {
private static MembreSummaryResponse unMembre() {
return new MembreSummaryResponse(
- UUID.randomUUID(), null, "Prenom", "Nom", null, null, null, null, null, null, true, List.of(), null, null);
+ UUID.randomUUID(), null, "Prenom", "Nom", null, null, null, null, null, null, true, List.of(), null, null, null);
}
@Nested
diff --git a/src/test/java/dev/lions/unionflow/server/api/dto/membre/MembreSummaryResponseTest.java b/src/test/java/dev/lions/unionflow/server/api/dto/membre/MembreSummaryResponseTest.java
new file mode 100644
index 0000000..14d3696
--- /dev/null
+++ b/src/test/java/dev/lions/unionflow/server/api/dto/membre/MembreSummaryResponseTest.java
@@ -0,0 +1,266 @@
+package dev.lions.unionflow.server.api.dto.membre;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.UUID;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+@DisplayName("Tests MembreSummaryResponse — méthodes calculées")
+class MembreSummaryResponseTest {
+
+ private static MembreSummaryResponse make(String statut, List
+ * EN_ATTENTE_PAIEMENT → PAIEMENT_INITIE → PAIEMENT_CONFIRME → VALIDEE
+ * ↘ REJETEE
+ *
+ *
+ * @author UnionFlow Team
+ * @version 1.0
+ * @since 2026-03-30
+ */
+public enum StatutValidationSouscription {
+
+ EN_ATTENTE_PAIEMENT("En attente de paiement — souscription créée"),
+ PAIEMENT_INITIE("Paiement initié — session Wave ouverte"),
+ PAIEMENT_CONFIRME("Paiement confirmé — en attente de validation SuperAdmin"),
+ VALIDEE("Validée — compte activé"),
+ REJETEE("Rejetée — souscription refusée");
+
+ private final String libelle;
+
+ StatutValidationSouscription(String libelle) {
+ this.libelle = libelle;
+ }
+
+ public String getLibelle() {
+ return libelle;
+ }
+
+ /** Indique si la souscription est dans un état terminal (non modifiable). */
+ public boolean isTerminal() {
+ return this == VALIDEE || this == REJETEE;
+ }
+
+ /** Indique si un paiement Wave peut être initié depuis cet état. */
+ public boolean peutInitierPaiement() {
+ return this == EN_ATTENTE_PAIEMENT;
+ }
+}
diff --git a/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypeFormule.java b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypeFormule.java
index 0810e4d..6e9fb77 100644
--- a/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypeFormule.java
+++ b/src/main/java/dev/lions/unionflow/server/api/enums/abonnement/TypeFormule.java
@@ -1,17 +1,23 @@
package dev.lions.unionflow.server.api.enums.abonnement;
/**
- * Énumération des types de formules d'abonnement UnionFlow
+ * Niveaux de formule d'abonnement UnionFlow.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * @author UnionFlow Team
+ * @version 2.0
+ * @since 2026-03-30
+ */
public enum TypePeriodeAbonnement {
- MENSUEL("Mensuel"),
- ANNUEL("Annuel — 2 mois offerts");
+
+ MENSUEL("Mensuel", 1, new BigDecimal("1.00")),
+ TRIMESTRIEL("Trimestriel — 5% de remise", 3, new BigDecimal("0.95")),
+ SEMESTRIEL("Semestriel — 10% de remise", 6, new BigDecimal("0.90")),
+ ANNUEL("Annuel — 20% de remise (2 mois offerts)", 12, new BigDecimal("0.80"));
private final String libelle;
+ private final int nombreMois;
+ private final BigDecimal coefficient;
- TypePeriodeAbonnement(String libelle) { this.libelle = libelle; }
+ TypePeriodeAbonnement(String libelle, int nombreMois, BigDecimal coefficient) {
+ this.libelle = libelle;
+ this.nombreMois = nombreMois;
+ this.coefficient = coefficient;
+ }
- public String getLibelle() { return libelle; }
+ public String getLibelle() {
+ return libelle;
+ }
+
+ public int getNombreMois() {
+ return nombreMois;
+ }
+
+ public BigDecimal getCoefficient() {
+ return coefficient;
+ }
}
diff --git a/src/main/java/dev/lions/unionflow/server/api/enums/membre/StatutMembre.java b/src/main/java/dev/lions/unionflow/server/api/enums/membre/StatutMembre.java
index a4ca328..955f50f 100644
--- a/src/main/java/dev/lions/unionflow/server/api/enums/membre/StatutMembre.java
+++ b/src/main/java/dev/lions/unionflow/server/api/enums/membre/StatutMembre.java
@@ -8,6 +8,7 @@ package dev.lions.unionflow.server.api.enums.membre;
* @since 2025-01-10
*/
public enum StatutMembre {
+ INVITE("Invité — en attente d'acceptation"),
EN_ATTENTE_VALIDATION("En attente de validation"),
ACTIF("Actif"),
INACTIF("Inactif — cotisations en retard"),
@@ -15,7 +16,8 @@ public enum StatutMembre {
DEMISSIONNAIRE("Démissionnaire"),
RADIE("Radié — exclusion définitive"),
HONORAIRE("Honoraire — sans cotisation obligatoire"),
- DECEDE("Décédé — archivage / gestion ayants droit");
+ DECEDE("Décédé — archivage / gestion ayants droit"),
+ ARCHIVE("Archivé — données conservées, accès révoqué");
private final String libelle;
diff --git a/src/main/java/dev/lions/unionflow/server/api/enums/organisation/TypeOrganisation.java b/src/main/java/dev/lions/unionflow/server/api/enums/organisation/TypeOrganisation.java
index 363ea7c..751d2be 100644
--- a/src/main/java/dev/lions/unionflow/server/api/enums/organisation/TypeOrganisation.java
+++ b/src/main/java/dev/lions/unionflow/server/api/enums/organisation/TypeOrganisation.java
@@ -1,21 +1,26 @@
package dev.lions.unionflow.server.api.enums.organisation;
/**
- * Énumération des types d'organisations supportés par UnionFlow
+ * Types d'organisations supportées par UnionFlow.
+ *
+ *