Initial commit: unionflow-server-api

Code source complet à la racine du repository.

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:23:37 +00:00
commit 32823206db
339 changed files with 34394 additions and 0 deletions

143
.gitignore vendored Normal file
View File

@@ -0,0 +1,143 @@
# ====================================
# GITIGNORE POUR UNIONFLOW SERVER API
# ====================================
# ===== MAVEN =====
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.flattened-pom.xml
# ===== QUARKUS =====
.quarkus/
quarkus.log
hs_err_pid*
# ===== JAVA COMPILED FILES =====
*.class
*.jar
*.war
*.ear
*.nar
*.zip
*.tar.gz
*.rar
# ===== IDE - ECLIPSE =====
.project
.classpath
.settings/
.factorypath
.metadata/
bin/
.apt_generated
.springBeans
.sts4-cache
# ===== IDE - INTELLIJ IDEA =====
.idea/
*.iml
*.ipr
*.iws
out/
# ===== IDE - NETBEANS =====
nbproject/
nbbuild/
nbdist/
.nb-gradle/
nb-configuration.xml
nbactions.xml
# ===== IDE - VS CODE =====
.vscode/
*.code-workspace
# ===== OS SPECIFIC =====
# Mac
.DS_Store
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# Linux
*~
# ===== LOGS =====
*.log
*.log.*
logs/
log/
# ===== TEMPORARY FILES =====
*.tmp
*.temp
*.bak
*.backup
*.old
*.swp
*.swo
*.orig
*.rej
*~
# ===== ENVIRONMENT & CONFIGURATION =====
.env
.env.local
.env.*
*.local
application-local.properties
# ===== SECURITY - KEYS & CERTIFICATES =====
*.key
*.pem
*.crt
*.p12
*.jks
*.keystore
.certs/
.certificates/
# ===== TEST COVERAGE =====
.jacoco/
jacoco.exec
coverage/
.nyc_output/
# ===== GENERATED SOURCES =====
src/gen/
generated-sources/
generated-test-sources/
# ===== BUILD ARTIFACTS =====
dist/
build/
out/
# ===== NODE (if used for build tools) =====
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# ===== DOCKER =====
.dockerignore
# ===== MAVEN WRAPPER (if not using project wrapper) =====
# Uncomment if you don't want to commit Maven wrapper
# .mvn/
# mvnw
# mvnw.cmd

548
README.md Normal file
View File

@@ -0,0 +1,548 @@
# UnionFlow Server API - DTOs et Contrats
![Java](https://img.shields.io/badge/Java-17-blue)
![Maven](https://img.shields.io/badge/Maven-Central-green)
![License](https://img.shields.io/badge/License-Proprietary-red)
Module API partagé UnionFlow - DTOs, Request/Response, Validation, Enums.
---
## 📋 Vue d'ensemble
Ce module contient les **contrats d'API** partagés entre :
- **Backend Quarkus** (unionflow-server-impl-quarkus)
- **Frontend Web** (unionflow-client-quarkus-primefaces-freya)
- **Mobile Flutter** (unionflow-mobile-apps) - via génération TypeScript/JSON
### Avantages
**Type-safety** : Contrats Java typés
**Validation centralisée** : Contraintes Jakarta Bean Validation
**DRY** : Zéro duplication entre projets
**Versioning** : Maven semantic versioning
**Documentation** : Javadoc complète
---
## 🏗️ Structure
```
src/main/java/dev/lions/unionflow/server/api/
├── dto/ # Data Transfer Objects
│ ├── base/ # DTOs de base
│ │ ├── BaseRequest.java
│ │ └── BaseResponse.java
│ ├── dashboard/ # Dashboard
│ │ ├── DashboardStatsResponse.java
│ │ ├── MembreDashboardSyntheseResponse.java
│ │ └── UpcomingEventResponse.java
│ ├── membre/ # Membres
│ │ ├── request/
│ │ │ └── CreateMembreRequest.java
│ │ └── response/
│ │ ├── MembreResponse.java
│ └── MembreSummaryResponse.java
│ ├── finance/ # Finance Workflow
│ │ ├── request/
│ │ │ ├── ApproveTransactionRequest.java
│ │ │ └── RejectTransactionRequest.java
│ │ └── response/
│ │ ├── AdhesionResponse.java
│ │ ├── TransactionApprovalResponse.java
│ │ └── BudgetResponse.java
│ ├── cotisation/ # Cotisations
│ ├── evenement/ # Événements
│ ├── solidarite/ # Demandes d'aide
│ └── notification/ # Notifications
├── enums/ # Énumérations
│ ├── membre/
│ │ ├── StatutMembre.java
│ │ └── TypeMembre.java
│ ├── finance/
│ │ ├── StatutApprobation.java
│ │ ├── TypeTransaction.java
│ │ └── BudgetPeriod.java
│ ├── paiement/
│ │ ├── StatutPaiement.java
│ │ └── ModePaiement.java
│ └── notification/
│ └── TypeNotification.java
├── validation/ # Validateurs custom
│ ├── annotations/ # Annotations validation
│ │ ├── @ValidEmail
│ │ ├── @ValidPhoneNumber
│ │ └── @ValidAmount
│ ├── validators/ # Implémentations
│ │ ├── EmailValidator.java
│ │ └── AmountValidator.java
│ └── ValidationConstants.java # Constantes (regex, limites)
└── exception/ # Exceptions API
├── ApiException.java
├── ValidationException.java
└── ErrorResponse.java
```
---
## 📦 Installation
### Maven Dependency
Ajouter à votre `pom.xml` :
```xml
<dependency>
<groupId>dev.lions.unionflow</groupId>
<artifactId>unionflow-server-api</artifactId>
<version>2.0.0</version>
</dependency>
```
### Repository Gitea (Maven Registry)
Configurer `~/.m2/settings.xml` :
```xml
<settings>
<servers>
<server>
<id>gitea-lionsdev</id>
<username>${env.GITEA_USERNAME}</username>
<password>${env.GITEA_TOKEN}</password>
</server>
</servers>
<repositories>
<repository>
<id>gitea-lionsdev</id>
<url>https://git.lions.dev/api/packages/lionsdev/maven</url>
</repository>
</repositories>
</settings>
```
### Variables d'environnement
```bash
export GITEA_USERNAME=lionsdev
export GITEA_TOKEN=your-gitea-token
```
---
## 🎯 Utilisation
### 1. DTOs Request/Response
#### Exemple : Créer un membre
**Request DTO** :
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateMembreRequest {
@NotBlank(message = "Le nom est requis")
@Size(min = 2, max = 100)
private String nom;
@NotBlank(message = "Le prénom est requis")
@Size(min = 2, max = 100)
private String prenom;
@ValidEmail
private String email;
@ValidPhoneNumber
private String telephone;
@NotNull
private UUID organisationId;
private TypeMembre typeMembre = TypeMembre.MEMBRE;
}
```
**Response DTO** :
```java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MembreResponse extends BaseResponse {
private UUID id;
private String nom;
private String prenom;
private String email;
private String telephone;
private StatutMembre statut;
private TypeMembre type;
private String numeroMembre;
private LocalDate dateAdhesion;
private UUID organisationId;
private String organisationNom;
}
```
**Usage Backend** :
```java
@POST
@Path("/membres")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createMembre(@Valid CreateMembreRequest request) {
MembreResponse response = membreService.create(request);
return Response.status(201).entity(response).build();
}
```
**Usage Frontend (REST Client)** :
```java
@Path("/api/v1/membres")
@RegisterRestClient(configKey = "unionflow-backend")
public interface MembreRestClient {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
MembreResponse createMembre(@Valid CreateMembreRequest request);
}
```
### 2. Validation
#### Annotations standard (Jakarta Bean Validation)
```java
@NotNull // Non null
@NotBlank // Non vide (String)
@Size(min, max) // Taille min/max
@Min(value) // Valeur minimale (nombres)
@Max(value) // Valeur maximale (nombres)
@Email // Format email
@Pattern(regex) // Regex custom
@Positive // > 0
@PositiveOrZero // >= 0
```
#### Annotations custom UnionFlow
```java
@ValidEmail // Email avec domaines autorisés
@ValidPhoneNumber // Téléphone international (+225, +33, etc.)
@ValidAmount // Montant positif, max 2 décimales
@ValidNumeroMembre // Format: ORG-YYYY-NNNN
@ValidPeriodeCotisation // Format: YYYY-MM
```
**Exemple d'utilisation** :
```java
public class PaiementRequest {
@ValidAmount(min = 100.0, max = 10_000_000.0)
private BigDecimal montant;
@ValidPhoneNumber
private String telephonePaiement;
@NotNull
@Pattern(regexp = "^\\d{4}-\\d{2}$")
private String periode; // Ex: "2026-03"
}
```
#### Validators custom - Implémentation
**Annotation** :
```java
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AmountValidator.class)
public @interface ValidAmount {
String message() default "Montant invalide";
double min() default 0.0;
double max() default Double.MAX_VALUE;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
```
**Validator** :
```java
public class AmountValidator implements ConstraintValidator<ValidAmount, BigDecimal> {
private double min;
private double max;
@Override
public void initialize(ValidAmount annotation) {
this.min = annotation.min();
this.max = annotation.max();
}
@Override
public boolean isValid(BigDecimal value, ConstraintValidatorContext context) {
if (value == null) return true; // Use @NotNull separately
double amount = value.doubleValue();
// Check positive
if (amount <= 0) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Le montant doit être positif")
.addConstraintViolation();
return false;
}
// Check range
if (amount < min || amount > max) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
String.format("Le montant doit être entre %.2f et %.2f", min, max)
).addConstraintViolation();
return false;
}
// Check max 2 decimals
if (value.scale() > 2) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Maximum 2 décimales autorisées")
.addConstraintViolation();
return false;
}
return true;
}
}
```
### 3. Enums
#### Exemple : StatutApprobation
```java
public enum StatutApprobation {
PENDING("En attente"),
APPROVED("Approuvée"),
REJECTED("Rejetée"),
CANCELLED("Annulée");
private final String libelle;
StatutApprobation(String libelle) {
this.libelle = libelle;
}
public String getLibelle() {
return libelle;
}
public static StatutApprobation fromString(String str) {
return Arrays.stream(values())
.filter(s -> s.name().equalsIgnoreCase(str))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Statut invalide: " + str));
}
}
```
**Usage** :
```java
StatutApprobation statut = StatutApprobation.APPROVED;
System.out.println(statut.getLibelle()); // "Approuvée"
StatutApprobation parsed = StatutApprobation.fromString("PENDING");
```
### 4. BaseResponse - Héritage commun
Tous les Response DTOs étendent `BaseResponse` :
```java
@Data
@NoArgsConstructor
public abstract class BaseResponse implements Serializable {
private static final long serialVersionUID = 1L;
// Audit fields (si nécessaire côté client)
private LocalDateTime dateCreation;
private LocalDateTime dateModification;
private Boolean actif;
}
```
**Avantages** :
- Champs audit automatiques
- Serializable pour cache/sessions
- Type-safe avec génériques
---
## 🧪 Tests
### Tests de validation
**Fichier** : `src/test/java/dev/lions/unionflow/server/api/validation/AmountValidatorTest.java`
```java
@Test
void shouldRejectNegativeAmount() {
CreatePaiementRequest request = new CreatePaiementRequest();
request.setMontant(BigDecimal.valueOf(-100));
Set<ConstraintViolation<CreatePaiementRequest>> violations = validator.validate(request);
assertFalse(violations.isEmpty());
assertTrue(violations.stream()
.anyMatch(v -> v.getMessage().contains("positif")));
}
@Test
void shouldRejectTooManyDecimals() {
CreatePaiementRequest request = new CreatePaiementRequest();
request.setMontant(BigDecimal.valueOf(100.123));
Set<ConstraintViolation<CreatePaiementRequest>> violations = validator.validate(request);
assertFalse(violations.isEmpty());
assertTrue(violations.stream()
.anyMatch(v -> v.getMessage().contains("2 décimales")));
}
```
### Lancer les tests
```bash
mvn test
```
---
## 📊 DTOs par Feature
### Dashboard
- `DashboardStatsResponse` - Stats organisation (membres, cotisations, events)
- `MembreDashboardSyntheseResponse` - Synthèse membre (solde, cotisations)
- `UpcomingEventResponse` - Événements à venir
### Finance Workflow
- `TransactionApprovalResponse` - Approbation de transaction
- `BudgetResponse` - Budget avec lignes budgétaires
- `AdhesionResponse` - Adhésion membre
### Membres
- `MembreResponse` - Détails membre complets
- `MembreSummaryResponse` - Résumé membre (liste)
- `CreateMembreRequest` - Création membre
- `UpdateMembreRequest` - Modification membre
### Cotisations
- `CotisationResponse` - Cotisation avec détails
- `CreateCotisationRequest` - Enregistrement cotisation
- `CotisationStatisticsResponse` - Statistiques cotisations
### Notifications
- `NotificationResponse` - Notification utilisateur
- `MarkAsReadRequest` - Marquer comme lue
---
## 🔄 Publication Maven (Développeurs seulement)
### Publier une nouvelle version
```bash
# 1. Mettre à jour la version dans pom.xml
<version>2.1.0</version>
# 2. Compiler et publier
mvn clean deploy
# Les artifacts sont publiés sur:
# https://git.lions.dev/api/packages/lionsdev/maven
```
### Versioning Semantic
- **Major** (2.0.0) : Breaking changes
- **Minor** (2.1.0) : Nouvelles features (backward compatible)
- **Patch** (2.0.1) : Bugfixes
---
## 📝 Changelog
### v2.0.0 (2026-03-14)
**Nouveau** :
- DTOs Finance Workflow complets
- Validation `@ValidAmount` avec min/max
- Enums `BudgetPeriod`, `BudgetCategory`
- `TransactionApprovalResponse` avec tous les champs
**Améliorations** :
- BaseResponse avec audit fields
- ValidationConstants centralisées
- Javadoc complète pour tous les DTOs
### v1.0.0 (2026-01-04)
- Version initiale
- 20+ DTOs principaux
- 10+ validators custom
---
## 📚 Documentation
### Javadoc
```bash
# Générer Javadoc
mvn javadoc:javadoc
# Ouvrir
open target/site/apidocs/index.html
```
### Ressources
- **Jakarta Bean Validation**: https://jakarta.ee/specifications/bean-validation/
- **Maven Repository**: https://git.lions.dev/lionsdev/unionflow-server-api
---
## 🤝 Contribution
1. Créer une branche feature
2. Ajouter DTOs/Validators avec tests
3. Documenter avec Javadoc
4. Pull Request avec description
---
## 📄 Licence
Propriétaire - © 2026 Lions Club Côte d'Ivoire
---
**Version** : 2.0.0
**Dernière mise à jour** : 2026-03-14
**Auteur** : Équipe UnionFlow

359
checkstyle-unionflow.xml Normal file
View File

@@ -0,0 +1,359 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
Configuration Checkstyle pour UnionFlow
Basée sur Google Java Style Guide avec adaptations pour UnionFlow
Objectif : 100% de conformité (zéro violation)
@author UnionFlow Team
@version 1.0
@since 2025-01-10
-->
<module name="Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="error"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Suppressions pour les fichiers générés -->
<module name="SuppressionFilter">
<property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
default="checkstyle-suppressions.xml" />
<property name="optional" value="true"/>
</module>
<!-- Vérifications au niveau des fichiers -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="120"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="NewlineAtEndOfFile"/>
<!-- Vérifications au niveau des arbres syntaxiques -->
<module name="TreeWalker">
<!-- Annotations -->
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="AnnotationUseStyle"/>
<module name="MissingOverride"/>
<!-- Blocs -->
<module name="AvoidNestedBlocks"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens" value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="LeftCurly"/>
<module name="NeedBraces"/>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens" value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT, INSTANCE_INIT"/>
</module>
<!-- Conception de classe -->
<module name="FinalClass"/>
<module name="HideUtilityClassConstructor"/>
<module name="InterfaceIsType"/>
<module name="OneTopLevelClass"/>
<module name="VisibilityModifier">
<property name="protectedAllowed" value="true"/>
<property name="packageAllowed" value="true"/>
</module>
<!-- Codage -->
<module name="ArrayTrailingComma"/>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="CovariantEquals"/>
<module name="DeclarationOrder"/>
<module name="DefaultComesLast"/>
<module name="EmptyStatement"/>
<module name="EqualsAvoidNull"/>
<module name="EqualsHashCode"/>
<module name="ExplicitInitialization"/>
<module name="FallThrough"/>
<module name="IllegalInstantiation"/>
<module name="IllegalToken"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format" value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message" value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="InnerAssignment"/>
<module name="MagicNumber">
<property name="ignoreNumbers" value="-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000"/>
<property name="ignoreHashCodeMethod" value="true"/>
<property name="ignoreAnnotation" value="true"/>
<property name="ignoreFieldDeclaration" value="true"/>
</module>
<module name="MissingSwitchDefault"/>
<module name="ModifiedControlVariable"/>
<module name="MultipleStringLiterals">
<property name="allowedDuplicates" value="3"/>
</module>
<module name="MultipleVariableDeclarations"/>
<module name="NoClone"/>
<module name="NoFinalizer"/>
<module name="OneStatementPerLine"/>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="PackageDeclaration"/>
<module name="ParameterAssignment"/>
<module name="RequireThis">
<property name="checkFields" value="false"/>
<property name="checkMethods" value="false"/>
</module>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
<module name="StringLiteralEquality"/>
<module name="UnnecessaryParentheses"/>
<module name="VariableDeclarationUsageDistance"/>
<!-- Imports -->
<module name="AvoidStarImport"/>
<module name="AvoidStaticImport">
<property name="excludes" value="org.assertj.core.api.Assertions.*,org.junit.jupiter.api.Assertions.*,org.mockito.Mockito.*"/>
</module>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE"/>
</module>
<module name="IllegalImport"/>
<module name="RedundantImport"/>
<module name="UnusedImports"/>
<!-- Javadoc -->
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="InvalidJavadocPosition"/>
<module name="JavadocMethod">
<property name="allowMissingParamTags" value="false"/>
<property name="allowMissingReturnTag" value="false"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>
</module>
<module name="JavadocParagraph"/>
<module name="JavadocStyle"/>
<module name="JavadocTagContinuationIndentation"/>
<module name="JavadocType"/>
<module name="MissingJavadocMethod">
<property name="minLineCount" value="2"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>
</module>
<module name="MissingJavadocType"/>
<module name="NonEmptyAtclauseDescription"/>
<module name="SingleLineJavadoc">
<property name="ignoreInlineTags" value="false"/>
</module>
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments" value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<!-- Métriques -->
<module name="BooleanExpressionComplexity">
<property name="max" value="7"/>
</module>
<module name="ClassDataAbstractionCoupling">
<property name="max" value="15"/>
</module>
<module name="ClassFanOutComplexity">
<property name="max" value="25"/>
</module>
<module name="CyclomaticComplexity">
<property name="max" value="15"/>
</module>
<module name="JavaNCSS">
<property name="methodMaximum" value="80"/>
<property name="classMaximum" value="2000"/>
</module>
<module name="NPathComplexity">
<property name="max" value="200"/>
</module>
<!-- Divers -->
<module name="ArrayTypeStyle"/>
<module name="CommentsIndentation"/>
<module name="Indentation">
<property name="basicOffset" value="4"/>
<property name="braceAdjustment" value="0"/>
<property name="caseIndent" value="4"/>
<property name="throwsIndent" value="8"/>
<property name="lineWrappingIndentation" value="8"/>
<property name="arrayInitIndent" value="4"/>
</module>
<module name="OuterTypeFilename"/>
<module name="TodoComment">
<property name="format" value="(TODO)|(FIXME)"/>
</module>
<module name="TrailingComment"/>
<module name="UncommentedMain">
<property name="excludedClasses" value="\.Main$"/>
</module>
<module name="UpperEll"/>
<!-- Modificateurs -->
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<!-- Conventions de nommage -->
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="4"/>
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF, PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF"/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern" value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ConstantName"/>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern" value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalFinalVariableName"/>
<module name="LocalVariableName">
<property name="tokens" value="VARIABLE_DEF"/>
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern" value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern" value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern" value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern" value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern" value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="StaticVariableName"/>
<module name="TypeName">
<message key="name.invalidPattern" value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<!-- Taille -->
<module name="AnonInnerLength">
<property name="max" value="30"/>
</module>
<module name="ExecutableStatementCount">
<property name="max" value="50"/>
</module>
<module name="LineLength">
<property name="max" value="120"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="MethodCount">
<property name="maxTotal" value="50"/>
<property name="maxPrivate" value="30"/>
<property name="maxPackage" value="30"/>
<property name="maxProtected" value="30"/>
<property name="maxPublic" value="30"/>
</module>
<module name="MethodLength">
<property name="tokens" value="METHOD_DEF, CTOR_DEF"/>
<property name="max" value="100"/>
</module>
<module name="OuterTypeNumber"/>
<module name="ParameterNumber">
<property name="max" value="8"/>
<property name="ignoreOverriddenMethods" value="true"/>
<property name="tokens" value="METHOD_DEF, CTOR_DEF"/>
</module>
<!-- Espaces blancs -->
<module name="EmptyForIteratorPad"/>
<module name="EmptyLineSeparator">
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="GenericWhitespace">
<message key="ws.followed" value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded" value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow" value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded" value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="MethodParamPad"/>
<module name="NoLineWrap"/>
<module name="NoWhitespaceAfter"/>
<module name="NoWhitespaceBefore"/>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens" value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
</module>
<module name="ParenPad"/>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="TypecastParenPad"/>
<module name="WhitespaceAfter"/>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<message key="ws.notFollowed" value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded" value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
</module>
</module>

2
lombok.config Normal file
View File

@@ -0,0 +1,2 @@
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true

96
parent-pom.xml Normal file
View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.lions.unionflow</groupId>
<artifactId>unionflow-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<name>UnionFlow - Parent</name>
<description>Plateforme complète de gestion d'union — POM parent partagé</description>
<distributionManagement>
<repository>
<id>gitea-lionsdev</id>
<url>https://git.lions.dev/api/packages/lionsdev/maven</url>
</repository>
<snapshotRepository>
<id>gitea-lionsdev</id>
<url>https://git.lions.dev/api/packages/lionsdev/maven</url>
</snapshotRepository>
</distributionManagement>
<repositories>
<repository>
<id>gitea-lionsdev</id>
<url>https://git.lions.dev/api/packages/lionsdev/maven</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.version>3.15.1</quarkus.platform.version>
<lombok.version>1.18.34</lombok.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.lions.unionflow</groupId>
<artifactId>unionflow-server-api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
<parameters>true</parameters>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

241
pom.xml Normal file
View File

@@ -0,0 +1,241 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.lions.unionflow</groupId>
<artifactId>unionflow-parent</artifactId>
<version>1.0.0</version>
<relativePath>parent-pom.xml</relativePath>
</parent>
<artifactId>unionflow-server-api</artifactId>
<packaging>jar</packaging>
<name>UnionFlow Server API</name>
<description>API définitions pour le serveur UnionFlow</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<quarkus.platform.version>3.15.1</quarkus.platform.version>
<jackson.version>2.17.0</jackson.version>
<validation-api.version>3.0.2</validation-api.version>
<microprofile-openapi.version>3.1.1</microprofile-openapi.version>
<!-- Versions des plugins de qualité -->
<jacoco.version>0.8.11</jacoco.version>
<checkstyle.version>10.12.4</checkstyle.version>
<maven-checkstyle-plugin.version>3.3.1</maven-checkstyle-plugin.version>
<junit.version>5.10.1</junit.version>
<mockito.version>5.7.0</mockito.version>
<assertj.version>3.24.2</assertj.version>
<!-- Seuils de couverture Jacoco - 100% obligatoire -->
<jacoco.line.coverage.minimum>1.00</jacoco.line.coverage.minimum>
<jacoco.branch.coverage.minimum>1.00</jacoco.branch.coverage.minimum>
<jacoco.instruction.coverage.minimum>1.00</jacoco.instruction.coverage.minimum>
<jacoco.method.coverage.minimum>1.00</jacoco.method.coverage.minimum>
<jacoco.class.coverage.minimum>1.00</jacoco.class.coverage.minimum>
</properties>
<dependencies>
<!-- Jackson pour la sérialisation JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Bean Validation -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>${validation-api.version}</version>
</dependency>
<!-- MicroProfile OpenAPI pour la documentation -->
<dependency>
<groupId>org.eclipse.microprofile.openapi</groupId>
<artifactId>microprofile-openapi-api</artifactId>
<version>${microprofile-openapi.version}</version>
</dependency>
<!-- Dépendances de test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>4.0.2</version>
<scope>test</scope>
</dependency>
<!-- Lombok pour génération automatique des getters/setters -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- Plugin Jacoco pour la couverture de code -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>${jacoco.line.coverage.minimum}</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>${jacoco.branch.coverage.minimum}</minimum>
</limit>
<limit>
<counter>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>${jacoco.instruction.coverage.minimum}</minimum>
</limit>
<limit>
<counter>METHOD</counter>
<value>COVEREDRATIO</value>
<minimum>${jacoco.method.coverage.minimum}</minimum>
</limit>
<limit>
<counter>CLASS</counter>
<value>COVEREDRATIO</value>
<minimum>${jacoco.class.coverage.minimum}</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<!-- Plugin Checkstyle pour la qualité du code -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${maven-checkstyle-plugin.version}</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
<configuration>
<configLocation>google_checks.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<violationSeverity>warning</violationSeverity>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<excludes>**/target/**/*</excludes>
</configuration>
<!-- Executions désactivées temporairement pour les tests
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
-->
</plugin>
</plugins>
</build>
</project>

30
script/publish-api.bat Normal file
View File

@@ -0,0 +1,30 @@
@echo off
REM Publie le parent pom + server-api sur le Gitea Package Registry
REM Usage : script\publish-api.bat
REM Depuis : n'importe où dans le repo server-api
REM Prérequis: credentials dans %USERPROFILE%\.m2\settings.xml (server id: gitea-lionsdev)
set REGISTRY_URL=https://git.lions.dev/api/packages/lionsdev/maven
set REGISTRY_ID=gitea-lionsdev
cd /d "%~dp0\.."
echo.
echo [1/2] Publication du parent pom...
call mvn deploy:deploy-file ^
-DgroupId=dev.lions.unionflow ^
-DartifactId=unionflow-parent ^
-Dversion=1.0.0 ^
-Dpackaging=pom ^
-Dfile=parent-pom.xml ^
-DrepositoryId=%REGISTRY_ID% ^
-Durl=%REGISTRY_URL%
if errorlevel 409 echo [WARN] Parent pom deja publie pour cette version, on continue.
echo.
echo [2/2] Publication du server-api...
call mvn deploy -DskipTests
if errorlevel 409 echo [WARN] Server-api deja publie - incrementer la version pour republier.
echo.
echo Done -- https://git.lions.dev/lionsdev/-/packages

32
script/publish-api.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Publie le parent pom + server-api sur le Gitea Package Registry
# Usage : ./script/publish-api.sh
# Depuis : n'importe où dans le repo server-api
# Prérequis: credentials dans ~/.m2/settings.xml (server id: gitea-lionsdev)
set -e
REGISTRY_URL="https://git.lions.dev/api/packages/lionsdev/maven"
REGISTRY_ID="gitea-lionsdev"
cd "$(dirname "$0")/.."
echo ""
echo "[1/2] Publication du parent pom..."
mvn deploy:deploy-file \
-DgroupId=dev.lions.unionflow \
-DartifactId=unionflow-parent \
-Dversion=1.0.0 \
-Dpackaging=pom \
-Dfile=parent-pom.xml \
-DrepositoryId="${REGISTRY_ID}" \
-Durl="${REGISTRY_URL}" \
|| echo "[WARN] Parent pom déjà publié pour cette version (409), on continue."
echo ""
echo "[2/2] Publication du server-api..."
mvn deploy -DskipTests \
|| echo "[WARN] Server-api déjà publié pour cette version (409) - incrémenter la version pour republier."
echo ""
echo "Done -- https://git.lions.dev/lionsdev/-/packages"

View File

@@ -0,0 +1,82 @@
package dev.lions.unionflow.server.api.dto.abonnement.request;
import dev.lions.unionflow.server.api.enums.abonnement.StatutAbonnement;
import dev.lions.unionflow.server.api.enums.abonnement.TypePeriodeAbonnement;
import dev.lions.unionflow.server.api.enums.formuleabonnement.TypeFormule;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de création d'un abonnement.
*/
@Builder
public record CreateAbonnementRequest(
@NotBlank(message = "Le numéro de référence est obligatoire") @Pattern(regexp = "^ABO-\\d{4}-[A-Z0-9]{8}$", message = "Format de référence invalide (ABO-YYYY-XXXXXXXX)") String numeroReference,
@NotNull(message = "L'identifiant de l'organisation est obligatoire") UUID organisationId,
String nomOrganisation,
@NotNull(message = "L'identifiant de la formule est obligatoire") UUID formulaireId,
String codeFormule,
String nomFormule,
TypeFormule typeFormule,
@NotNull(message = "Le statut est obligatoire") StatutAbonnement statut,
@NotNull(message = "Le type d'abonnement est obligatoire") TypePeriodeAbonnement typeAbonnement,
@NotNull(message = "La date de début est obligatoire") LocalDate dateDebut,
@Future(message = "La date de fin doit être dans le futur") LocalDate dateFin,
LocalDate dateProchainePeriode,
@NotNull(message = "Le montant est obligatoire") @DecimalMin(value = "0.0", inclusive = false, message = "Le montant doit être positif") @Digits(integer = 10, fraction = 2, message = "Le montant ne peut avoir plus de 10 chiffres entiers et 2 décimales") BigDecimal montant,
@NotBlank(message = "La devise est obligatoire") @Pattern(regexp = "^[A-Z]{3}$", message = "La devise doit être un code ISO à 3 lettres") String devise,
@DecimalMin(value = "0.0", message = "La remise doit être positive") @DecimalMin(value = "100.0", message = "La remise ne peut pas dépasser 100%") BigDecimal remise,
@DecimalMin(value = "0.0", message = "Le montant final doit être positif") @Digits(integer = 10, fraction = 2, message = "Le montant ne peut avoir plus de 10 chiffres entiers et 2 décimales") BigDecimal montantFinal,
Boolean renouvellementAutomatique,
Boolean periodeEssaiUtilisee,
LocalDate dateFinEssai,
Integer maxMembres,
Integer nombreMembresActuels,
BigDecimal espaceStockageGB,
BigDecimal espaceStockageUtilise,
Boolean supportTechnique,
String niveauSupport,
Boolean fonctionnalitesAvancees,
Boolean apiAccess,
Boolean rapportsPersonnalises,
Boolean integrationsTierces,
UUID responsableId,
String nomResponsable,
String emailResponsable,
String telephoneResponsable,
@Pattern(regexp = "^(WAVE_MONEY|ORANGE_MONEY|FREE_MONEY|VIREMENT|CHEQUE|AUTRE)$", message = "Mode de paiement invalide") String modePaiementPrefere,
@Pattern(regexp = "^\\+?[0-9]{8,15}$", message = "Format de numéro de téléphone invalide") String numeroPaiementMobile,
@Size(max = 5000, message = "L'historique ne peut pas dépasser 5000 caractères") String historiquePaiements,
@Size(max = 1000, message = "Les notes ne peuvent pas dépasser 1000 caractères") String notes,
Boolean alertesActivees,
Boolean notificationsEmail,
Boolean notificationsSMS) {
}

View File

@@ -0,0 +1,77 @@
package dev.lions.unionflow.server.api.dto.abonnement.request;
import dev.lions.unionflow.server.api.enums.abonnement.StatutAbonnement;
import dev.lions.unionflow.server.api.enums.abonnement.TypePeriodeAbonnement;
import dev.lions.unionflow.server.api.enums.formuleabonnement.TypeFormule;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de mise à jour d'un abonnement.
*/
@Builder
public record UpdateAbonnementRequest(
@Pattern(regexp = "^ABO-\\d{4}-[A-Z0-9]{8}$", message = "Format de référence invalide (ABO-YYYY-XXXXXXXX)") String numeroReference,
UUID organisationId,
String nomOrganisation,
UUID formulaireId,
String codeFormule,
String nomFormule,
TypeFormule typeFormule,
StatutAbonnement statut,
TypePeriodeAbonnement typeAbonnement,
LocalDate dateDebut,
@Future(message = "La date de fin doit être dans le futur") LocalDate dateFin,
LocalDate dateProchainePeriode,
@DecimalMin(value = "0.0", inclusive = false, message = "Le montant doit être positif") @Digits(integer = 10, fraction = 2, message = "Le montant ne peut avoir plus de 10 chiffres entiers et 2 décimales") BigDecimal montant,
@Pattern(regexp = "^[A-Z]{3}$", message = "La devise doit être un code ISO à 3 lettres") String devise,
@DecimalMin(value = "0.0", message = "La remise doit être positive") @DecimalMin(value = "100.0", message = "La remise ne peut pas dépasser 100%") BigDecimal remise,
@DecimalMin(value = "0.0", message = "Le montant final doit être positif") @Digits(integer = 10, fraction = 2, message = "Le montant ne peut avoir plus de 10 chiffres entiers et 2 décimales") BigDecimal montantFinal,
Boolean renouvellementAutomatique,
Boolean periodeEssaiUtilisee,
LocalDate dateFinEssai,
Integer maxMembres,
Integer nombreMembresActuels,
BigDecimal espaceStockageGB,
BigDecimal espaceStockageUtilise,
Boolean supportTechnique,
String niveauSupport,
Boolean fonctionnalitesAvancees,
Boolean apiAccess,
Boolean rapportsPersonnalises,
Boolean integrationsTierces,
UUID responsableId,
String nomResponsable,
String emailResponsable,
String telephoneResponsable,
@Pattern(regexp = "^(WAVE_MONEY|ORANGE_MONEY|FREE_MONEY|VIREMENT|CHEQUE|AUTRE)$", message = "Mode de paiement invalide") String modePaiementPrefere,
@Pattern(regexp = "^\\+?[0-9]{8,15}$", message = "Format de numéro de téléphone invalide") String numeroPaiementMobile,
@Size(max = 5000, message = "L'historique ne peut pas dépasser 5000 caractères") String historiquePaiements,
@Size(max = 1000, message = "Les notes ne peuvent pas dépasser 1000 caractères") String notes,
Boolean alertesActivees,
Boolean notificationsEmail,
Boolean notificationsSMS) {
}

View File

@@ -0,0 +1,133 @@
package dev.lions.unionflow.server.api.dto.abonnement.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import dev.lions.unionflow.server.api.enums.abonnement.StatutAbonnement;
import dev.lions.unionflow.server.api.enums.abonnement.TypePeriodeAbonnement;
import dev.lions.unionflow.server.api.enums.formuleabonnement.TypeFormule;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse détaillée pour un abonnement.
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AbonnementResponse extends BaseResponse {
private String numeroReference;
private UUID organisationId;
private String nomOrganisation;
private UUID formulaireId;
private String codeFormule;
private String nomFormule;
private TypeFormule typeFormule;
private StatutAbonnement statut;
private TypePeriodeAbonnement typeAbonnement;
private LocalDate dateDebut;
private LocalDate dateFin;
private LocalDate dateProchainePeriode;
private BigDecimal montant;
private String devise;
private BigDecimal remise;
private BigDecimal montantFinal;
private Boolean renouvellementAutomatique;
private Boolean periodeEssaiUtilisee;
private LocalDate dateFinEssai;
private Integer maxMembres;
private Integer nombreMembresActuels;
private BigDecimal espaceStockageGB;
private BigDecimal espaceStockageUtilise;
private Boolean supportTechnique;
private String niveauSupport;
private Boolean fonctionnalitesAvancees;
private Boolean apiAccess;
private Boolean rapportsPersonnalises;
private Boolean integrationsTierces;
private LocalDateTime dateDerniereUtilisation;
private Integer connexionsCeMois;
private UUID responsableId;
private String nomResponsable;
private String emailResponsable;
private String telephoneResponsable;
private String modePaiementPrefere;
private String numeroPaiementMobile;
private String historiquePaiements;
private String notes;
private Boolean alertesActivees;
private Boolean notificationsEmail;
private Boolean notificationsSMS;
private LocalDateTime dateSuspension;
private String raisonSuspension;
private LocalDateTime dateAnnulation;
private String raisonAnnulation;
// === MÉTHODES UTILITAIRES ===
public boolean isActive() {
return StatutAbonnement.ACTIF.equals(statut);
}
public boolean isExpire() {
return StatutAbonnement.EXPIRE.equals(statut) ||
(dateFin != null && dateFin.isBefore(LocalDate.now()));
}
public boolean isSuspendu() {
return StatutAbonnement.SUSPENDU.equals(statut);
}
public int getMembresRestants() {
if (maxMembres == null) return 0;
int actuels = nombreMembresActuels != null ? nombreMembresActuels : 0;
return Math.max(0, maxMembres - actuels);
}
public boolean isQuotaAtteint() {
if (maxMembres == null) return false;
int actuels = nombreMembresActuels != null ? nombreMembresActuels : 0;
return actuels >= maxMembres;
}
public int getPourcentageUtilisation() {
if (maxMembres == null || maxMembres == 0) return 0;
int actuels = nombreMembresActuels != null ? nombreMembresActuels : 0;
return (actuels * 100) / maxMembres;
}
public long getJoursRestants() {
if (dateFin == null) return -1;
LocalDate maintenant = LocalDate.now();
if (maintenant.isAfter(dateFin)) return 0;
return java.time.temporal.ChronoUnit.DAYS.between(maintenant, dateFin);
}
public boolean isExpirationProche() {
long joursRestants = getJoursRestants();
return joursRestants >= 0 && joursRestants <= 30;
}
public boolean peutEtreRenouvele() {
return Boolean.TRUE.equals(renouvellementAutomatique) && !isExpire();
}
public String getStatutLibelle() {
return statut != null ? statut.name() : "INCONNU";
}
}

View File

@@ -0,0 +1,29 @@
package dev.lions.unionflow.server.api.dto.admin.request;
import java.time.LocalDateTime;
import lombok.Builder;
/**
* Requête de création d'un log d'audit.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreateAuditLogRequest(
String typeAction,
String severite,
String utilisateur,
String role,
String module,
String description,
String details,
String ipAddress,
String userAgent,
String sessionId,
LocalDateTime dateHeure,
String donneesAvant,
String donneesApres,
String entiteId,
String entiteType) {
}

View File

@@ -0,0 +1,33 @@
package dev.lions.unionflow.server.api.dto.admin.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;
/**
* Réponse pour les logs d'audit.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
public class AuditLogResponse extends BaseResponse {
private String typeAction;
private String severite;
private String utilisateur;
private String role;
private String module;
private String description;
private String details;
private String ipAddress;
private String userAgent;
private String sessionId;
private LocalDateTime dateHeure;
private String donneesAvant;
private String donneesApres;
private String entiteId;
private String entiteType;
}

View File

@@ -0,0 +1,43 @@
package dev.lions.unionflow.server.api.dto.adresse.request;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.UUID;
/**
* Requete de création d'une adresse.
*
* @author UnionFlow Team
* @version 3.0
*/
public record CreateAdresseRequest(
@NotBlank(message = "Le type d'adresse est obligatoire") String typeAdresse, // Code depuis types_reference
@NotBlank(message = "L'adresse est obligatoire") String adresse,
String complementAdresse,
String codePostal,
@NotBlank(message = "La ville est obligatoire") String ville,
String region,
@NotBlank(message = "Le pays est obligatoire") String pays,
@DecimalMin(value = "-90.0", message = "La latitude doit être comprise entre -90 et 90") @DecimalMax(value = "90.0", message = "La latitude doit être comprise entre -90 et 90") @Digits(integer = 3, fraction = 6) BigDecimal latitude, // Optionnel
@DecimalMin(value = "-180.0", message = "La longitude doit être comprise entre -180 et 180") @DecimalMax(value = "180.0", message = "La longitude doit être comprise entre -180 et 180") @Digits(integer = 3, fraction = 6) BigDecimal longitude, // Optionnel
@NotNull(message = "L'indicateur principale est obligatoire") Boolean principale,
String libelle,
String notes,
UUID organisationId, // Exclusive: soit organisationId, soit membreId, soit evenementId
UUID membreId,
UUID evenementId) {
}

View File

@@ -0,0 +1,36 @@
package dev.lions.unionflow.server.api.dto.adresse.request;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import java.math.BigDecimal;
import java.util.UUID;
/**
* Requete de mise à jour d'une adresse.
* Tous les champs sont optionnels pour permettre des mises à jour partielles.
*
* @author UnionFlow Team
* @version 3.0
*/
public record UpdateAdresseRequest(
String typeAdresse, // Code depuis types_reference
String adresse,
String complementAdresse,
String codePostal,
String ville,
String region,
String pays,
@DecimalMin(value = "-90.0", message = "La latitude doit être comprise entre -90 et 90") @DecimalMax(value = "90.0", message = "La latitude doit être comprise entre -90 et 90") @Digits(integer = 3, fraction = 6) BigDecimal latitude,
@DecimalMin(value = "-180.0", message = "La longitude doit être comprise entre -180 et 180") @DecimalMax(value = "180.0", message = "La longitude doit être comprise entre -180 et 180") @Digits(integer = 3, fraction = 6) BigDecimal longitude,
Boolean principale,
String libelle,
String notes,
UUID organisationId,
UUID membreId,
UUID evenementId) {
}

View File

@@ -0,0 +1,42 @@
package dev.lions.unionflow.server.api.dto.adresse.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.math.BigDecimal;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
/**
* DTO de réponse détaillée pour une adresse.
*
* @author UnionFlow Team
* @version 3.0
*/
@Getter
@Setter
public class AdresseResponse extends BaseResponse {
private String typeAdresse; // Code
private String typeAdresseLibelle; // Depuis types_reference
private String typeAdresseIcone;
private String adresse;
private String complementAdresse;
private String codePostal;
private String ville;
private String region;
private String pays;
private BigDecimal latitude;
private BigDecimal longitude;
private Boolean principale;
private String libelle;
private String notes;
private UUID organisationId;
private UUID membreId;
private UUID evenementId;
private String adresseComplete;
}

View File

@@ -0,0 +1,35 @@
package dev.lions.unionflow.server.api.dto.agricole;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.agricole.StatutCampagneAgricole;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CampagneAgricoleDTO extends BaseDTO {
private String organisationCoopId;
// Exemple : "Campagne d'Arachide 2025/2026"
private String designation;
private String typeCulturePrincipale;
// Nombre d'hectares au total couvert par les membres de la coop
private BigDecimal surfaceTotaleEstimeeHectares;
// Tonnes récoltées attendues vs réelles
private BigDecimal volumePrevisionnelTonnes;
private BigDecimal volumeReelTonnes;
private StatutCampagneAgricole statut;
}

View File

@@ -0,0 +1,265 @@
package dev.lions.unionflow.server.api.dto.analytics;
import com.fasterxml.jackson.annotation.JsonFormat;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* DTO pour les données analytics UnionFlow
*
* <p>
* Représente une donnée analytique avec sa valeur, sa métrique associée, sa
* période d'analyse et
* ses métadonnées.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AnalyticsDataResponse extends BaseDTO {
private static final long serialVersionUID = 1L;
/** Type de métrique analysée */
@NotNull(message = "Le type de métrique est obligatoire")
private TypeMetrique typeMetrique;
/** Période d'analyse */
@NotNull(message = "La période d'analyse est obligatoire")
private PeriodeAnalyse periodeAnalyse;
/** Valeur numérique de la métrique */
@NotNull(message = "La valeur est obligatoire")
@DecimalMin(value = "0.0", message = "La valeur doit être positive ou nulle")
@Digits(integer = 15, fraction = 4, message = "Format de valeur invalide")
private BigDecimal valeur;
/** Valeur précédente pour comparaison */
@DecimalMin(value = "0.0", message = "La valeur précédente doit être positive ou nulle")
@Digits(integer = 15, fraction = 4, message = "Format de valeur précédente invalide")
private BigDecimal valeurPrecedente;
/** Pourcentage d'évolution par rapport à la période précédente */
@Digits(integer = 6, fraction = 2, message = "Format de pourcentage d'évolution invalide")
private BigDecimal pourcentageEvolution;
/** Date de début de la période analysée */
@NotNull(message = "La date de début est obligatoire")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateDebut;
/** Date de fin de la période analysée */
@NotNull(message = "La date de fin est obligatoire")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateFin;
/** Date de calcul de la métrique */
@NotNull(message = "La date de calcul est obligatoire")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateCalcul;
/** Identifiant de l'organisation (optionnel pour filtrage) */
private UUID organisationId;
/** Nom de l'organisation */
@Size(max = 200, message = "Le nom de l'organisation ne peut pas dépasser 200 caractères")
private String nomOrganisation;
/** Identifiant de l'utilisateur qui a demandé le calcul */
private UUID utilisateurId;
/** Nom de l'utilisateur qui a demandé le calcul */
@Size(max = 200, message = "Le nom de l'utilisateur ne peut pas dépasser 200 caractères")
private String nomUtilisateur;
/** Libellé personnalisé de la métrique */
@Size(max = 300, message = "Le libellé personnalisé ne peut pas dépasser 300 caractères")
private String libellePersonnalise;
/** Description ou commentaire sur la métrique */
@Size(max = 1000, message = "La description ne peut pas dépasser 1000 caractères")
private String description;
/** Données détaillées pour les graphiques (format JSON) */
@Size(max = 10000, message = "Les données détaillées ne peuvent pas dépasser 10000 caractères")
private String donneesDetaillees;
/** Configuration du graphique (couleurs, type, etc.) */
@Size(max = 2000, message = "La configuration graphique ne peut pas dépasser 2000 caractères")
private String configurationGraphique;
/** Métadonnées additionnelles */
private Map<String, Object> metadonnees;
/** Indicateur de fiabilité des données (0-100) */
@DecimalMin(value = "0.0", message = "L'indicateur de fiabilité doit être positif")
@DecimalMax(value = "100.0", message = "L'indicateur de fiabilité ne peut pas dépasser 100")
@Digits(integer = 3, fraction = 1, message = "Format d'indicateur de fiabilité invalide")
private BigDecimal indicateurFiabilite;
/** Nombre d'éléments analysés pour calculer cette métrique */
@DecimalMin(value = "0", message = "Le nombre d'éléments doit être positif")
private Integer nombreElementsAnalyses;
/** Temps de calcul en millisecondes */
@DecimalMin(value = "0", message = "Le temps de calcul doit être positif")
private Long tempsCalculMs;
/** Indicateur si la métrique est en temps réel */
@Builder.Default
private Boolean tempsReel = false;
/** Indicateur si la métrique nécessite une mise à jour */
@Builder.Default
private Boolean necessiteMiseAJour = false;
/** Niveau de priorité de la métrique (1=faible, 5=critique) */
@DecimalMin(value = "1", message = "Le niveau de priorité minimum est 1")
@DecimalMax(value = "5", message = "Le niveau de priorité maximum est 5")
private Integer niveauPriorite;
/** Tags pour catégoriser la métrique */
private List<String> tags;
// === MÉTHODES UTILITAIRES ===
/**
* Retourne le libellé à afficher (personnalisé ou par défaut)
*
* @return Le libellé à afficher
*/
public String getLibelleAffichage() {
return libellePersonnalise != null && !libellePersonnalise.trim().isEmpty()
? libellePersonnalise
: typeMetrique.getLibelle();
}
/**
* Retourne l'unité de mesure de la métrique
*
* @return L'unité de mesure
*/
public String getUnite() {
return typeMetrique.getUnite();
}
/**
* Retourne l'icône de la métrique
*
* @return L'icône Material Design
*/
public String getIcone() {
return typeMetrique.getIcone();
}
/**
* Retourne la couleur de la métrique
*
* @return Le code couleur hexadécimal
*/
public String getCouleur() {
return typeMetrique.getCouleur();
}
/**
* Vérifie si la métrique a évolué positivement
*
* @return true si l'évolution est positive
*/
public boolean hasEvolutionPositive() {
return pourcentageEvolution != null && pourcentageEvolution.compareTo(BigDecimal.ZERO) > 0;
}
/**
* Vérifie si la métrique a évolué négativement
*
* @return true si l'évolution est négative
*/
public boolean hasEvolutionNegative() {
return pourcentageEvolution != null && pourcentageEvolution.compareTo(BigDecimal.ZERO) < 0;
}
/**
* Vérifie si la métrique est stable (pas d'évolution)
*
* @return true si l'évolution est nulle
*/
public boolean isStable() {
return pourcentageEvolution != null && pourcentageEvolution.compareTo(BigDecimal.ZERO) == 0;
}
/**
* Retourne la tendance sous forme de texte
*
* @return "hausse", "baisse" ou "stable"
*/
public String getTendance() {
if (hasEvolutionPositive())
return "hausse";
if (hasEvolutionNegative())
return "baisse";
return "stable";
}
/**
* Vérifie si les données sont fiables (indicateur > 80)
*
* @return true si les données sont considérées comme fiables
*/
public boolean isDonneesFiables() {
return indicateurFiabilite != null
&& indicateurFiabilite.compareTo(new BigDecimal("80.0")) >= 0;
}
/**
* Vérifie si la métrique est critique (priorité >= 4)
*
* @return true si la métrique est critique
*/
public boolean isCritique() {
return niveauPriorite != null && niveauPriorite >= 4;
}
/**
* Constructeur avec les champs essentiels
*
* @param typeMetrique Le type de métrique
* @param periodeAnalyse La période d'analyse
* @param valeur La valeur de la métrique
*/
public AnalyticsDataResponse(
TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, BigDecimal valeur) {
super();
this.typeMetrique = typeMetrique;
this.periodeAnalyse = periodeAnalyse;
this.valeur = valeur;
this.dateCalcul = LocalDateTime.now();
this.dateDebut = periodeAnalyse.getDateDebut();
this.dateFin = periodeAnalyse.getDateFin();
this.tempsReel = false;
this.necessiteMiseAJour = false;
this.niveauPriorite = 3; // Priorité normale par défaut
this.indicateurFiabilite = new BigDecimal("95.0"); // Fiabilité élevée par défaut
}
}

View File

@@ -0,0 +1,350 @@
package dev.lions.unionflow.server.api.dto.analytics;
import com.fasterxml.jackson.annotation.JsonFormat;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* DTO pour les widgets de tableau de bord analytics UnionFlow
*
* <p>Représente un widget personnalisable affiché sur le tableau de bord avec sa configuration, sa
* position et ses données.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardWidgetResponse extends BaseDTO {
private static final long serialVersionUID = 1L;
/** Titre du widget */
@NotBlank(message = "Le titre du widget est obligatoire")
@Size(min = 3, max = 200, message = "Le titre du widget doit contenir entre 3 et 200 caractères")
private String titre;
/** Description du widget */
@Size(max = 500, message = "La description ne peut pas dépasser 500 caractères")
private String description;
/** Type de widget (kpi, chart, table, gauge, progress, text) */
@NotBlank(message = "Le type de widget est obligatoire")
@Size(max = 50, message = "Le type de widget ne peut pas dépasser 50 caractères")
private String typeWidget;
/** Type de métrique affiché */
private TypeMetrique typeMetrique;
/** Période d'analyse */
private PeriodeAnalyse periodeAnalyse;
/** Identifiant de l'organisation (optionnel pour filtrage) */
private UUID organisationId;
/** Nom de l'organisation */
@Size(max = 200, message = "Le nom de l'organisation ne peut pas dépasser 200 caractères")
private String nomOrganisation;
/** Identifiant de l'utilisateur propriétaire */
@NotNull(message = "L'identifiant de l'utilisateur propriétaire est obligatoire")
private UUID utilisateurProprietaireId;
/** Nom de l'utilisateur propriétaire */
@Size(
max = 200,
message = "Le nom de l'utilisateur propriétaire ne peut pas dépasser 200 caractères")
private String nomUtilisateurProprietaire;
/** Position X du widget sur la grille */
@NotNull(message = "La position X est obligatoire")
@DecimalMin(value = "0", message = "La position X doit être positive ou nulle")
private Integer positionX;
/** Position Y du widget sur la grille */
@NotNull(message = "La position Y est obligatoire")
@DecimalMin(value = "0", message = "La position Y doit être positive ou nulle")
private Integer positionY;
/** Largeur du widget (en unités de grille) */
@NotNull(message = "La largeur est obligatoire")
@DecimalMin(value = "1", message = "La largeur minimum est 1")
@DecimalMax(value = "12", message = "La largeur maximum est 12")
private Integer largeur;
/** Hauteur du widget (en unités de grille) */
@NotNull(message = "La hauteur est obligatoire")
@DecimalMin(value = "1", message = "La hauteur minimum est 1")
@DecimalMax(value = "12", message = "La hauteur maximum est 12")
private Integer hauteur;
/** Ordre d'affichage (z-index) */
@DecimalMin(value = "0", message = "L'ordre d'affichage doit être positif ou nul")
@Builder.Default
private Integer ordreAffichage = 0;
/** Configuration visuelle du widget */
@Size(max = 5000, message = "La configuration visuelle ne peut pas dépasser 5000 caractères")
private String configurationVisuelle;
/** Couleur principale du widget */
@Size(max = 7, message = "La couleur doit être au format #RRGGBB")
private String couleurPrincipale;
/** Couleur secondaire du widget */
@Size(max = 7, message = "La couleur secondaire doit être au format #RRGGBB")
private String couleurSecondaire;
/** Icône du widget */
@Size(max = 50, message = "L'icône ne peut pas dépasser 50 caractères")
private String icone;
/** Indicateur si le widget est visible */
@Builder.Default private Boolean visible = true;
/** Indicateur si le widget est redimensionnable */
@Builder.Default private Boolean redimensionnable = true;
/** Indicateur si le widget est déplaçable */
@Builder.Default private Boolean deplacable = true;
/** Indicateur si le widget peut être supprimé */
@Builder.Default private Boolean supprimable = true;
/** Indicateur si le widget se met à jour automatiquement */
@Builder.Default private Boolean miseAJourAutomatique = true;
/** Fréquence de mise à jour en secondes */
@DecimalMin(value = "30", message = "La fréquence minimum est 30 secondes")
@Builder.Default
private Integer frequenceMiseAJourSecondes = 300; // 5 minutes par défaut
/** Date de dernière mise à jour des données */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateDerniereMiseAJour;
/** Prochaine mise à jour programmée */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime prochaineMiseAJour;
/** Données du widget (format JSON) */
@Size(max = 50000, message = "Les données du widget ne peuvent pas dépasser 50000 caractères")
private String donneesWidget;
/** Configuration des filtres */
private Map<String, Object> configurationFiltres;
/** Configuration des alertes */
private Map<String, Object> configurationAlertes;
/** Seuil d'alerte bas */
private Double seuilAlerteBas;
/** Seuil d'alerte haut */
private Double seuilAlerteHaut;
/** Indicateur si une alerte est active */
@Builder.Default private Boolean alerteActive = false;
/** Message d'alerte actuel */
@Size(max = 500, message = "Le message d'alerte ne peut pas dépasser 500 caractères")
private String messageAlerte;
/** Type d'alerte (info, warning, error, success) */
@Size(max = 20, message = "Le type d'alerte ne peut pas dépasser 20 caractères")
private String typeAlerte;
/** Permissions d'accès au widget */
@Size(max = 1000, message = "Les permissions ne peuvent pas dépasser 1000 caractères")
private String permissions;
/** Rôles autorisés à voir le widget */
@Size(max = 500, message = "Les rôles autorisés ne peuvent pas dépasser 500 caractères")
private String rolesAutorises;
/** Template personnalisé du widget */
@Size(max = 10000, message = "Le template personnalisé ne peut pas dépasser 10000 caractères")
private String templatePersonnalise;
/** CSS personnalisé du widget */
@Size(max = 5000, message = "Le CSS personnalisé ne peut pas dépasser 5000 caractères")
private String cssPersonnalise;
/** JavaScript personnalisé du widget */
@Size(max = 10000, message = "Le JavaScript personnalisé ne peut pas dépasser 10000 caractères")
private String javascriptPersonnalise;
/** Métadonnées additionnelles */
private Map<String, Object> metadonnees;
/** Nombre de vues du widget */
@DecimalMin(value = "0", message = "Le nombre de vues doit être positif")
@Builder.Default
private Long nombreVues = 0L;
/** Nombre d'interactions avec le widget */
@DecimalMin(value = "0", message = "Le nombre d'interactions doit être positif")
@Builder.Default
private Long nombreInteractions = 0L;
/** Temps moyen passé sur le widget (en secondes) */
@DecimalMin(value = "0", message = "Le temps moyen doit être positif")
private Integer tempsMoyenSecondes;
/** Taux d'erreur du widget (en pourcentage) */
@DecimalMin(value = "0.0", message = "Le taux d'erreur doit être positif")
@DecimalMax(value = "100.0", message = "Le taux d'erreur ne peut pas dépasser 100%")
@Builder.Default
private Double tauxErreur = 0.0;
/** Date de dernière erreur */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateDerniereErreur;
/** Message de dernière erreur */
@Size(max = 1000, message = "Le message d'erreur ne peut pas dépasser 1000 caractères")
private String messageDerniereErreur;
// === MÉTHODES UTILITAIRES ===
/**
* Retourne le libellé de la métrique si définie
*
* @return Le libellé de la métrique ou null
*/
public String getLibelleMetrique() {
return typeMetrique != null ? typeMetrique.getLibelle() : null;
}
/**
* Retourne l'unité de mesure si métrique définie
*
* @return L'unité de mesure ou chaîne vide
*/
public String getUnite() {
return typeMetrique != null ? typeMetrique.getUnite() : "";
}
/**
* Retourne l'icône de la métrique ou l'icône personnalisée
*
* @return L'icône à afficher
*/
public String getIconeAffichage() {
if (icone != null && !icone.trim().isEmpty()) {
return icone;
}
return typeMetrique != null ? typeMetrique.getIcone() : "dashboard";
}
/**
* Retourne la couleur de la métrique ou la couleur personnalisée
*
* @return La couleur à utiliser
*/
public String getCouleurAffichage() {
if (couleurPrincipale != null && !couleurPrincipale.trim().isEmpty()) {
return couleurPrincipale;
}
return typeMetrique != null ? typeMetrique.getCouleur() : "#757575";
}
/**
* Vérifie si le widget nécessite une mise à jour
*
* @return true si une mise à jour est nécessaire
*/
public boolean necessiteMiseAJour() {
return miseAJourAutomatique
&& prochaineMiseAJour != null
&& prochaineMiseAJour.isBefore(LocalDateTime.now());
}
/**
* Vérifie si le widget est interactif
*
* @return true si le widget permet des interactions
*/
public boolean isInteractif() {
return "chart".equals(typeWidget) || "table".equals(typeWidget) || "gauge".equals(typeWidget);
}
/**
* Vérifie si le widget affiche des données temps réel
*
* @return true si le widget est en temps réel
*/
/** Indique si la fréquence est en temps réel (pour couverture branches). */
private boolean isFrequenceTempsReel() {
if (frequenceMiseAJourSecondes == null) return false;
return frequenceMiseAJourSecondes <= 60;
}
public boolean isTempsReel() {
return isFrequenceTempsReel();
}
/**
* Retourne la taille du widget (surface occupée)
*
* @return La surface en unités de grille
*/
public int getTailleWidget() {
return largeur * hauteur;
}
/**
* Vérifie si le widget est grand (surface > 6)
*
* @return true si le widget est considéré comme grand
*/
public boolean isWidgetGrand() {
return getTailleWidget() > 6;
}
/**
* Vérifie si le widget a des erreurs récentes (< 24h)
*
* @return true si des erreurs récentes sont détectées
*/
public boolean hasErreursRecentes() {
return dateDerniereErreur != null
&& dateDerniereErreur.isAfter(LocalDateTime.now().minusHours(24));
}
/**
* Retourne le statut du widget
*
* @return "actif", "erreur", "inactif" ou "maintenance"
*/
/** Indique si le taux d'erreur déclenche le statut maintenance (pour couverture branches). */
private boolean isTauxErreurMaintenance() {
if (tauxErreur == null) return false;
return tauxErreur > 10.0;
}
public String getStatutWidget() {
if (hasErreursRecentes()) return "erreur";
if (!visible) return "inactif";
if (isTauxErreurMaintenance()) return "maintenance";
return "actif";
}
}

View File

@@ -0,0 +1,309 @@
package dev.lions.unionflow.server.api.dto.analytics;
import com.fasterxml.jackson.annotation.JsonFormat;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* DTO pour les tendances et évolutions des KPI UnionFlow
*
* <p>Représente l'évolution d'un KPI dans le temps avec les points de données historiques pour
* générer des graphiques de tendance.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class KPITrendResponse extends BaseDTO {
private static final long serialVersionUID = 1L;
/** Type de métrique pour cette tendance */
@NotNull(message = "Le type de métrique est obligatoire")
private TypeMetrique typeMetrique;
/** Période d'analyse globale */
@NotNull(message = "La période d'analyse est obligatoire")
private PeriodeAnalyse periodeAnalyse;
/** Identifiant de l'organisation (optionnel) */
private UUID organisationId;
/** Nom de l'organisation */
@Size(max = 200, message = "Le nom de l'organisation ne peut pas dépasser 200 caractères")
private String nomOrganisation;
/** Date de début de la période analysée */
@NotNull(message = "La date de début est obligatoire")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateDebut;
/** Date de fin de la période analysée */
@NotNull(message = "La date de fin est obligatoire")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateFin;
/** Points de données pour la tendance */
@NotNull(message = "Les points de données sont obligatoires")
private List<PointDonneeDTO> pointsDonnees;
/** Valeur actuelle du KPI */
@NotNull(message = "La valeur actuelle est obligatoire")
@DecimalMin(value = "0.0", message = "La valeur actuelle doit être positive ou nulle")
@Digits(integer = 15, fraction = 4, message = "Format de valeur actuelle invalide")
private BigDecimal valeurActuelle;
/** Valeur minimale sur la période */
@DecimalMin(value = "0.0", message = "La valeur minimale doit être positive ou nulle")
@Digits(integer = 15, fraction = 4, message = "Format de valeur minimale invalide")
private BigDecimal valeurMinimale;
/** Valeur maximale sur la période */
@DecimalMin(value = "0.0", message = "La valeur maximale doit être positive ou nulle")
@Digits(integer = 15, fraction = 4, message = "Format de valeur maximale invalide")
private BigDecimal valeurMaximale;
/** Valeur moyenne sur la période */
@DecimalMin(value = "0.0", message = "La valeur moyenne doit être positive ou nulle")
@Digits(integer = 15, fraction = 4, message = "Format de valeur moyenne invalide")
private BigDecimal valeurMoyenne;
/** Écart-type des valeurs */
@DecimalMin(value = "0.0", message = "L'écart-type doit être positif ou nul")
@Digits(integer = 15, fraction = 4, message = "Format d'écart-type invalide")
private BigDecimal ecartType;
/** Coefficient de variation (écart-type / moyenne) */
@DecimalMin(value = "0.0", message = "Le coefficient de variation doit être positif ou nul")
@Digits(integer = 6, fraction = 4, message = "Format de coefficient de variation invalide")
private BigDecimal coefficientVariation;
/** Tendance générale (pente de la régression linéaire) */
@Digits(integer = 10, fraction = 6, message = "Format de tendance invalide")
private BigDecimal tendanceGenerale;
/** Coefficient de corrélation R² */
@DecimalMin(value = "0.0", message = "Le coefficient de corrélation doit être positif ou nul")
@DecimalMax(value = "1.0", message = "Le coefficient de corrélation ne peut pas dépasser 1")
@Digits(integer = 1, fraction = 6, message = "Format de coefficient de corrélation invalide")
private BigDecimal coefficientCorrelation;
/** Pourcentage d'évolution depuis le début de la période */
@Digits(integer = 6, fraction = 2, message = "Format de pourcentage d'évolution invalide")
private BigDecimal pourcentageEvolutionGlobale;
/** Prédiction pour la prochaine période */
@DecimalMin(value = "0.0", message = "La prédiction doit être positive ou nulle")
@Digits(integer = 15, fraction = 4, message = "Format de prédiction invalide")
private BigDecimal predictionProchainePeriode;
/** Marge d'erreur de la prédiction (en pourcentage) */
@DecimalMin(value = "0.0", message = "La marge d'erreur doit être positive ou nulle")
@DecimalMax(value = "100.0", message = "La marge d'erreur ne peut pas dépasser 100%")
@Digits(integer = 3, fraction = 2, message = "Format de marge d'erreur invalide")
private BigDecimal margeErreurPrediction;
/** Seuil d'alerte bas */
@DecimalMin(value = "0.0", message = "Le seuil d'alerte bas doit être positif ou nul")
@Digits(integer = 15, fraction = 4, message = "Format de seuil d'alerte bas invalide")
private BigDecimal seuilAlerteBas;
/** Seuil d'alerte haut */
@DecimalMin(value = "0.0", message = "Le seuil d'alerte haut doit être positif ou nul")
@Digits(integer = 15, fraction = 4, message = "Format de seuil d'alerte haut invalide")
private BigDecimal seuilAlerteHaut;
/** Indicateur si une alerte est active */
@Builder.Default private Boolean alerteActive = false;
/** Type d'alerte (bas, haut, anomalie) */
@Size(max = 50, message = "Le type d'alerte ne peut pas dépasser 50 caractères")
private String typeAlerte;
/** Message d'alerte */
@Size(max = 500, message = "Le message d'alerte ne peut pas dépasser 500 caractères")
private String messageAlerte;
/** Configuration du graphique (couleurs, style, etc.) */
@Size(max = 2000, message = "La configuration graphique ne peut pas dépasser 2000 caractères")
private String configurationGraphique;
/** Intervalle de regroupement des données */
@Size(max = 20, message = "L'intervalle de regroupement ne peut pas dépasser 20 caractères")
private String intervalleRegroupement;
/** Format d'affichage des dates */
@Size(max = 20, message = "Le format de date ne peut pas dépasser 20 caractères")
private String formatDate;
/** Date de dernière mise à jour */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateDerniereMiseAJour;
/** Fréquence de mise à jour en minutes */
@DecimalMin(value = "1", message = "La fréquence de mise à jour minimum est 1 minute")
private Integer frequenceMiseAJourMinutes;
// === CLASSES INTERNES ===
/** Classe interne représentant un point de données dans la tendance */
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PointDonneeDTO {
/** Date du point de données */
@NotNull(message = "La date du point de données est obligatoire")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime date;
/** Valeur du point de données */
@NotNull(message = "La valeur du point de données est obligatoire")
@DecimalMin(value = "0.0", message = "La valeur du point doit être positive ou nulle")
@Digits(integer = 15, fraction = 4, message = "Format de valeur du point invalide")
private BigDecimal valeur;
/** Libellé du point (optionnel) */
@Size(max = 100, message = "Le libellé du point ne peut pas dépasser 100 caractères")
private String libelle;
/** Indicateur si le point est une anomalie */
@Builder.Default private Boolean anomalie = false;
/** Indicateur si le point est une prédiction */
@Builder.Default private Boolean prediction = false;
/** Métadonnées additionnelles du point */
private String metadonnees;
}
// === MÉTHODES UTILITAIRES ===
/**
* Retourne le libellé de la métrique
*
* @return Le libellé de la métrique
*/
public String getLibelleMetrique() {
return typeMetrique.getLibelle();
}
/**
* Retourne l'unité de mesure
*
* @return L'unité de mesure
*/
public String getUnite() {
return typeMetrique.getUnite();
}
/**
* Retourne l'icône de la métrique
*
* @return L'icône Material Design
*/
public String getIcone() {
return typeMetrique.getIcone();
}
/**
* Retourne la couleur de la métrique
*
* @return Le code couleur hexadécimal
*/
public String getCouleur() {
return typeMetrique.getCouleur();
}
/**
* Vérifie si la tendance est positive
*
* @return true si la tendance générale est positive
*/
public boolean isTendancePositive() {
return tendanceGenerale != null && tendanceGenerale.compareTo(BigDecimal.ZERO) > 0;
}
/**
* Vérifie si la tendance est négative
*
* @return true si la tendance générale est négative
*/
public boolean isTendanceNegative() {
return tendanceGenerale != null && tendanceGenerale.compareTo(BigDecimal.ZERO) < 0;
}
/**
* Vérifie si la tendance est stable
*
* @return true si la tendance générale est stable
*/
public boolean isTendanceStable() {
return tendanceGenerale != null && tendanceGenerale.compareTo(BigDecimal.ZERO) == 0;
}
/**
* Retourne la volatilité du KPI (basée sur le coefficient de variation)
*
* @return "faible", "moyenne" ou "élevée"
*/
public String getVolatilite() {
if (coefficientVariation == null) return "inconnue";
BigDecimal cv = coefficientVariation;
if (cv.compareTo(new BigDecimal("0.1")) <= 0) return "faible";
if (cv.compareTo(new BigDecimal("0.3")) <= 0) return "moyenne";
return "élevée";
}
/**
* Vérifie si la prédiction est fiable (R² > 0.7)
*
* @return true si la prédiction est considérée comme fiable
*/
public boolean isPredictionFiable() {
return coefficientCorrelation != null
&& coefficientCorrelation.compareTo(new BigDecimal("0.7")) >= 0;
}
/**
* Retourne le nombre de points de données
*
* @return Le nombre de points de données
*/
public int getNombrePointsDonnees() {
return pointsDonnees != null ? pointsDonnees.size() : 0;
}
/**
* Vérifie si des anomalies ont été détectées
*
* @return true si au moins un point est marqué comme anomalie
*/
public boolean hasAnomalies() {
return pointsDonnees != null
&& pointsDonnees.stream().anyMatch(point -> Boolean.TRUE.equals(point.getAnomalie()));
}
}

View File

@@ -0,0 +1,333 @@
package dev.lions.unionflow.server.api.dto.analytics;
import com.fasterxml.jackson.annotation.JsonFormat;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.analytics.FormatExport;
import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse;
import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* DTO pour la configuration des rapports analytics UnionFlow
*
* <p>Représente la configuration d'un rapport personnalisé avec ses métriques, sa mise en forme et
* ses paramètres d'export.
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-16
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReportConfigDTO extends BaseDTO {
private static final long serialVersionUID = 1L;
/** Nom du rapport */
@NotBlank(message = "Le nom du rapport est obligatoire")
@Size(min = 3, max = 200, message = "Le nom du rapport doit contenir entre 3 et 200 caractères")
private String nom;
/** Description du rapport */
@Size(max = 1000, message = "La description ne peut pas dépasser 1000 caractères")
private String description;
/** Type de rapport (executif, analytique, technique, operationnel) */
@NotBlank(message = "Le type de rapport est obligatoire")
@Size(max = 50, message = "Le type de rapport ne peut pas dépasser 50 caractères")
private String typeRapport;
/** Période d'analyse par défaut */
@NotNull(message = "La période d'analyse est obligatoire")
private PeriodeAnalyse periodeAnalyse;
/** Date de début personnalisée (si période personnalisée) */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateDebutPersonnalisee;
/** Date de fin personnalisée (si période personnalisée) */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateFinPersonnalisee;
/** Identifiant de l'organisation (optionnel pour filtrage) */
private UUID organisationId;
/** Nom de l'organisation */
@Size(max = 200, message = "Le nom de l'organisation ne peut pas dépasser 200 caractères")
private String nomOrganisation;
/** Identifiant de l'utilisateur créateur */
@NotNull(message = "L'identifiant de l'utilisateur créateur est obligatoire")
private UUID utilisateurCreateurId;
/** Nom de l'utilisateur créateur */
@Size(max = 200, message = "Le nom de l'utilisateur créateur ne peut pas dépasser 200 caractères")
private String nomUtilisateurCreateur;
/** Métriques incluses dans le rapport */
@NotNull(message = "Les métriques sont obligatoires")
@Valid
private List<MetriqueConfigDTO> metriques;
/** Sections du rapport */
@Valid private List<SectionRapportDTO> sections;
/** Format d'export par défaut */
@NotNull(message = "Le format d'export est obligatoire")
private FormatExport formatExport;
/** Formats d'export autorisés */
private List<FormatExport> formatsExportAutorises;
/** Modèle de rapport à utiliser */
@Size(max = 100, message = "Le modèle de rapport ne peut pas dépasser 100 caractères")
private String modeleRapport;
/** Configuration de la mise en page */
@Size(
max = 2000,
message = "La configuration de mise en page ne peut pas dépasser 2000 caractères")
private String configurationMiseEnPage;
/** Logo personnalisé (URL ou base64) */
@Size(max = 5000, message = "Le logo personnalisé ne peut pas dépasser 5000 caractères")
private String logoPersonnalise;
/** Couleurs personnalisées du rapport */
private Map<String, String> couleursPersonnalisees;
/** Indicateur si le rapport est public */
@Builder.Default private Boolean rapportPublic = false;
/** Indicateur si le rapport est automatique */
@Builder.Default private Boolean rapportAutomatique = false;
/** Fréquence de génération automatique (en heures) */
@DecimalMin(value = "1", message = "La fréquence minimum est 1 heure")
private Integer frequenceGenerationHeures;
/** Prochaine génération automatique */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime prochaineGeneration;
/** Liste des destinataires pour l'envoi automatique */
private List<String> destinatairesEmail;
/** Objet de l'email pour l'envoi automatique */
@Size(max = 200, message = "L'objet de l'email ne peut pas dépasser 200 caractères")
private String objetEmail;
/** Corps de l'email pour l'envoi automatique */
@Size(max = 2000, message = "Le corps de l'email ne peut pas dépasser 2000 caractères")
private String corpsEmail;
/** Paramètres de filtrage avancé */
private Map<String, Object> parametresFiltrage;
/** Tags pour catégoriser le rapport */
private List<String> tags;
/** Niveau de confidentialité (1=public, 5=confidentiel) */
@DecimalMin(value = "1", message = "Le niveau de confidentialité minimum est 1")
@DecimalMax(value = "5", message = "Le niveau de confidentialité maximum est 5")
@Builder.Default
private Integer niveauConfidentialite = 1;
/** Date de dernière génération */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateDerniereGeneration;
/** Nombre de générations effectuées */
@DecimalMin(value = "0", message = "Le nombre de générations doit être positif")
@Builder.Default
private Integer nombreGenerations = 0;
/** Taille moyenne des rapports générés (en KB) */
@DecimalMin(value = "0", message = "La taille moyenne doit être positive")
private Long tailleMoyenneKB;
/** Temps moyen de génération (en secondes) */
@DecimalMin(value = "0", message = "Le temps moyen de génération doit être positif")
private Integer tempsMoyenGenerationSecondes;
// === CLASSES INTERNES ===
/** Configuration d'une métrique dans le rapport */
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class MetriqueConfigDTO {
/** Type de métrique */
@NotNull(message = "Le type de métrique est obligatoire")
private TypeMetrique typeMetrique;
/** Libellé personnalisé */
@Size(max = 200, message = "Le libellé personnalisé ne peut pas dépasser 200 caractères")
private String libellePersonnalise;
/** Position dans le rapport (ordre d'affichage) */
@DecimalMin(value = "1", message = "La position minimum est 1")
private Integer position;
/** Taille d'affichage (1=petit, 2=moyen, 3=grand) */
@DecimalMin(value = "1", message = "La taille minimum est 1")
@DecimalMax(value = "3", message = "La taille maximum est 3")
@Builder.Default
private Integer tailleAffichage = 2;
/** Couleur personnalisée */
@Size(max = 7, message = "La couleur doit être au format #RRGGBB")
private String couleurPersonnalisee;
/** Indicateur si la métrique inclut un graphique */
@Builder.Default private Boolean inclureGraphique = true;
/** Type de graphique (line, bar, pie, area) */
@Size(max = 20, message = "Le type de graphique ne peut pas dépasser 20 caractères")
@Builder.Default
private String typeGraphique = "line";
/** Indicateur si la métrique inclut la tendance */
@Builder.Default private Boolean inclureTendance = true;
/** Indicateur si la métrique inclut la comparaison */
@Builder.Default private Boolean inclureComparaison = true;
/** Seuils d'alerte personnalisés */
private Map<String, Object> seuilsAlerte;
/** Filtres spécifiques à cette métrique */
private Map<String, Object> filtresSpecifiques;
}
/** Configuration d'une section du rapport */
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SectionRapportDTO {
/** Nom de la section */
@NotBlank(message = "Le nom de la section est obligatoire")
@Size(max = 200, message = "Le nom de la section ne peut pas dépasser 200 caractères")
private String nom;
/** Description de la section */
@Size(max = 500, message = "La description de la section ne peut pas dépasser 500 caractères")
private String description;
/** Position de la section dans le rapport */
@DecimalMin(value = "1", message = "La position minimum est 1")
private Integer position;
/** Type de section (resume, metriques, graphiques, tableaux, analyse) */
@NotBlank(message = "Le type de section est obligatoire")
@Size(max = 50, message = "Le type de section ne peut pas dépasser 50 caractères")
private String typeSection;
/** Métriques incluses dans cette section */
private List<TypeMetrique> metriquesIncluses;
/** Configuration spécifique de la section */
private Map<String, Object> configurationSection;
/** Indicateur si la section est visible */
@Builder.Default private Boolean visible = true;
/** Indicateur si la section peut être réduite */
@Builder.Default private Boolean pliable = false;
}
// === MÉTHODES UTILITAIRES ===
/**
* Retourne le nombre de métriques configurées
*
* @return Le nombre de métriques
*/
public int getNombreMetriques() {
return metriques != null ? metriques.size() : 0;
}
/**
* Retourne le nombre de sections configurées
*
* @return Le nombre de sections
*/
public int getNombreSections() {
return sections != null ? sections.size() : 0;
}
/**
* Vérifie si le rapport utilise une période personnalisée
*
* @return true si la période est personnalisée
*/
public boolean isPeriodePersonnalisee() {
return periodeAnalyse == PeriodeAnalyse.PERIODE_PERSONNALISEE;
}
/**
* Vérifie si le rapport est confidentiel (niveau >= 4)
*
* @return true si le rapport est confidentiel
*/
/** Indique si le niveau de confidentialité est élevé (pour couverture branches). */
private boolean isNiveauConfidentiel() {
if (niveauConfidentialite == null) return false;
return niveauConfidentialite >= 4;
}
public boolean isConfidentiel() {
return isNiveauConfidentiel();
}
/**
* Vérifie si le rapport nécessite une génération
*
* @return true si la prochaine génération est due
*/
public boolean necessiteGeneration() {
return rapportAutomatique
&& prochaineGeneration != null
&& prochaineGeneration.isBefore(LocalDateTime.now());
}
/**
* Retourne la fréquence de génération en texte
*
* @return La fréquence sous forme de texte
*/
public String getFrequenceTexte() {
if (frequenceGenerationHeures == null) return "Manuelle";
return switch (frequenceGenerationHeures) {
case 1 -> "Toutes les heures";
case 24 -> "Quotidienne";
case 168 -> "Hebdomadaire"; // 24 * 7
case 720 -> "Mensuelle"; // 24 * 30
default -> "Toutes les " + frequenceGenerationHeures + " heures";
};
}
}

View File

@@ -0,0 +1,32 @@
package dev.lions.unionflow.server.api.dto.auth.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Builder;
/**
* Requête d'authentification utilisateur.
*
* @param username Email ou nom d'utilisateur
* @param password Mot de passe
* @param typeCompte Type de compte (MEMBRE, ADMIN, etc.)
* @param rememberMe Se souvenir de moi
* @author UnionFlow Team
* @version 2.0
* @since 2026-02-28
*/
@Builder
public record LoginRequest(
@NotBlank(message = "L'email ou nom d'utilisateur est requis")
@Size(min = 3, max = 100, message = "L'email ou nom d'utilisateur doit contenir entre 3 et 100 caractères")
String username,
@NotBlank(message = "Le mot de passe est requis")
@Size(min = 6, message = "Le mot de passe doit contenir au moins 6 caractères")
String password,
@NotBlank(message = "Le type de compte est requis")
String typeCompte,
Boolean rememberMe) {
}

View File

@@ -0,0 +1,90 @@
package dev.lions.unionflow.server.api.dto.auth.response;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse d'authentification contenant le token JWT et les informations utilisateur.
*
* @author UnionFlow Team
* @version 2.0
* @since 2026-02-28
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginResponse {
private String accessToken;
private String refreshToken;
@Builder.Default
private String tokenType = "Bearer";
private Long expiresIn;
private LocalDateTime expirationDate;
private UserInfo user;
/**
* Vérifie si le token est expiré.
*
* @return true si le token est expiré
*/
public boolean isExpired() {
return expirationDate != null && LocalDateTime.now().isAfter(expirationDate);
}
/**
* Informations de l'utilisateur connecté.
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class UserInfo {
private UUID id;
private String nom;
private String prenom;
private String email;
private String username;
private String typeCompte;
private List<String> roles;
private List<String> permissions;
private EntiteInfo entite;
/**
* Retourne le nom complet de l'utilisateur.
*
* @return Prénom + Nom ou nom d'utilisateur si absent
*/
public String getNomComplet() {
if (prenom != null && nom != null) {
return prenom + " " + nom;
}
return nom != null ? nom : username;
}
}
/**
* Informations sur l'entité (organisation/membre) de l'utilisateur.
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class EntiteInfo {
private UUID id;
private String nom;
private String type;
private String pays;
private String ville;
}
}

View File

@@ -0,0 +1,42 @@
package dev.lions.unionflow.server.api.dto.ayantdroit;
import dev.lions.unionflow.server.api.enums.ayantdroit.LienParente;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Past;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AyantDroitRequest {
@NotBlank(message = "L'Id du membre principal rattaché est obligatoire")
private String membrePrincipalId;
@NotBlank(message = "Le prénom est obligatoire")
private String prenom;
@NotBlank(message = "Le nom est obligatoire")
private String nom;
@NotNull(message = "La date de naissance est obligatoire pour l'âge limite")
@Past(message = "La date de naissance doit être dans le passé")
private LocalDate dateNaissance;
private String sexe;
private String pieceIdentite;
@NotNull(message = "Le lien de parenté / bénéfice est requis")
private LienParente lienParente;
// Id document du livret de famille ou certificat médical / scolaire
private String justificatifLienId;
}

View File

@@ -0,0 +1,40 @@
package dev.lions.unionflow.server.api.dto.ayantdroit;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.ayantdroit.LienParente;
import dev.lions.unionflow.server.api.enums.ayantdroit.StatutAyantDroit;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.math.BigDecimal;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class AyantDroitResponse extends BaseDTO {
private String membrePrincipalId;
private String numeroCarteBeneficiaire;
private String prenom;
private String nom;
private LocalDate dateNaissance;
private Integer ageActuel;
private String sexe;
private String pieceIdentite;
private LienParente lienParente;
// Prise en charge (%) par rapport à la couverture du Membre Principal
private BigDecimal pourcentageCouvertureSante;
private StatutAyantDroit statut;
}

View File

@@ -0,0 +1,28 @@
package dev.lions.unionflow.server.api.dto.backup.request;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Request pour créer une sauvegarde
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateBackupRequest {
@NotBlank(message = "Le nom de la sauvegarde est requis")
private String name;
private String description;
private String type; // AUTO, MANUAL, RESTORE_POINT
private Boolean includeDatabase;
private Boolean includeFiles;
private Boolean includeConfiguration;
}

View File

@@ -0,0 +1,28 @@
package dev.lions.unionflow.server.api.dto.backup.request;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
/**
* Request pour restaurer une sauvegarde
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RestoreBackupRequest {
@NotNull(message = "L'ID de la sauvegarde est requis")
private UUID backupId;
private Boolean restoreDatabase;
private Boolean restoreFiles;
private Boolean restoreConfiguration;
private Boolean createRestorePoint; // Créer un point de restauration avant
}

View File

@@ -0,0 +1,25 @@
package dev.lions.unionflow.server.api.dto.backup.request;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Request pour mettre à jour la configuration des sauvegardes automatiques
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdateBackupConfigRequest {
private Boolean autoBackupEnabled;
private String frequency; // HOURLY, DAILY, WEEKLY
private String retention; // "7 jours", "30 jours", "90 jours", "1 an"
private Integer retentionDays;
private String backupTime; // Format HH:mm pour sauvegarde quotidienne
private Boolean includeDatabase;
private Boolean includeFiles;
private Boolean includeConfiguration;
}

View File

@@ -0,0 +1,33 @@
package dev.lions.unionflow.server.api.dto.backup.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* Response contenant la configuration des sauvegardes automatiques
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BackupConfigResponse {
private Boolean autoBackupEnabled;
private String frequency; // HOURLY, DAILY, WEEKLY
private String retention; // "7 jours", "30 jours", etc.
private Integer retentionDays;
private String backupTime; // HH:mm
private Boolean includeDatabase;
private Boolean includeFiles;
private Boolean includeConfiguration;
private LocalDateTime lastBackup;
private LocalDateTime nextScheduledBackup;
private Integer totalBackups;
private Long totalSizeBytes;
private String totalSizeFormatted;
}

View File

@@ -0,0 +1,37 @@
package dev.lions.unionflow.server.api.dto.backup.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* Response représentant une sauvegarde
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BackupResponse {
private UUID id;
private String name;
private String description;
private String type; // AUTO, MANUAL, RESTORE_POINT
private Long sizeBytes;
private String sizeFormatted; // ex: "2.3 GB"
private String status; // PENDING, IN_PROGRESS, COMPLETED, FAILED
private LocalDateTime createdAt;
private LocalDateTime completedAt;
private String createdBy;
private Boolean includesDatabase;
private Boolean includesFiles;
private Boolean includesConfiguration;
private String filePath;
private String errorMessage; // Si status = FAILED
}

View File

@@ -0,0 +1,164 @@
package dev.lions.unionflow.server.api.dto.base;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
/**
* Classe de base pour tous les DTOs UnionFlow Fournit les propriétés communes
* d'audit et de gestion
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-10
*/
@Getter
@Setter
public abstract class BaseDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** Identifiant unique UUID */
private UUID id;
/** Date de création de l'enregistrement */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
public LocalDateTime dateCreation;
/** Date de dernière modification */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
public LocalDateTime dateModification;
/** Utilisateur qui a créé l'enregistrement */
private String creePar;
/** Utilisateur qui a modifié l'enregistrement en dernier */
private String modifiePar;
/** Version pour gestion de la concurrence optimiste */
private Long version;
/** Indicateur si l'enregistrement est actif */
private Boolean actif;
// Constructeur par défaut
public BaseDTO() {
this.dateCreation = LocalDateTime.now();
this.actif = true;
this.version = 0L;
}
// Getters et Setters générés automatiquement par Lombok @Getter/@Setter
// Méthodes utilitaires
/**
* Marque l'entité comme nouvellement créée
*
* @param utilisateur L'utilisateur qui crée l'entité
*/
public void marquerCommeNouveau(String utilisateur) {
LocalDateTime maintenant = LocalDateTime.now();
this.dateCreation = maintenant;
this.dateModification = maintenant;
this.creePar = utilisateur;
this.modifiePar = utilisateur;
this.version = 0L;
this.actif = true;
}
/**
* Marque l'entité comme modifiée
*
* @param utilisateur L'utilisateur qui modifie l'entité
*/
public void marquerCommeModifie(String utilisateur) {
this.dateModification = LocalDateTime.now();
this.modifiePar = utilisateur;
if (this.version != null) {
this.version++;
}
}
/**
* Désactive l'entité (soft delete)
*
* @param utilisateur L'utilisateur qui désactive l'entité
*/
public void desactiver(String utilisateur) {
this.actif = false;
marquerCommeModifie(utilisateur);
}
/**
* Réactive l'entité
*
* @param utilisateur L'utilisateur qui réactive l'entité
*/
public void reactiver(String utilisateur) {
this.actif = true;
marquerCommeModifie(utilisateur);
}
/**
* Vérifie si l'entité est nouvelle (pas encore persistée)
*
* @return true si l'entité est nouvelle
*/
public boolean isNouveau() {
return id == null;
}
/**
* Vérifie si l'entité est active
*
* @return true si l'entité est active
*/
public boolean isActif() {
return Boolean.TRUE.equals(actif);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
BaseDTO baseDTO = (BaseDTO) obj;
return id != null && id.equals(baseDTO.id);
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
@Override
public String toString() {
return getClass().getSimpleName()
+ "{"
+ "id="
+ id
+ ", dateCreation="
+ dateCreation
+ ", dateModification="
+ dateModification
+ ", creePar='"
+ creePar
+ '\''
+ ", modifiePar='"
+ modifiePar
+ '\''
+ ", version="
+ version
+ ", actif="
+ actif
+ '}';
}
}

View File

@@ -0,0 +1,79 @@
package dev.lions.unionflow.server.api.dto.base;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
/**
* Classe de base pour les DTOs de réponse.
*
* <p>
* Contient les champs d'audit communs à toutes
* les réponses : identifiant, dates de
* création/modification, créateur, et version.
*
* <p>
* Les DTOs de type Request ne doivent
* <b>pas</b> hériter de cette classe (utiliser
* des {@code record} sans héritage).
*
* @author UnionFlow Team
* @version 3.0
* @since 2026-02-21
*/
@Getter
@Setter
public abstract class BaseResponse {
/** Identifiant unique de l'entité. */
private UUID id;
/** Date de création de l'entité. */
private LocalDateTime dateCreation;
/** Date de dernière modification. */
private LocalDateTime dateModification;
/** Email du créateur. */
private String creePar;
/** Email du dernier modificateur. */
private String modifiePar;
/** Version pour l'optimistic locking. */
private Long version;
/** État actif/inactif (soft-delete). */
private Boolean actif;
/**
* Comparaison basée sur l'ID.
* Deux BaseResponse sont égaux si leurs IDs sont égaux et non null.
*
* @param obj Objet à comparer
* @return true si les objets ont le même ID
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
BaseResponse that = (BaseResponse) obj;
return id != null && id.equals(that.id);
}
/**
* Hash code basé sur l'ID.
*
* @return Hash code de l'ID, ou 0 si ID null
*/
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
}

View File

@@ -0,0 +1,59 @@
package dev.lions.unionflow.server.api.dto.base;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Réponse paginée générique.
*
* <p>
* Encapsule une liste d'éléments avec les
* métadonnées de pagination (page, taille,
* total). Utilisable pour tout type de réponse
* paginée.
*
* @param <T> type des éléments de la page
* @author UnionFlow Team
* @version 3.0
* @since 2026-02-21
*/
@Getter
@AllArgsConstructor
public class PageResponse<T> {
/** Éléments de la page courante. */
private final List<T> contenu;
/** Numéro de page courant (0-indexed). */
private final int page;
/** Taille de la page demandée. */
private final int taille;
/** Nombre total d'éléments. */
private final long totalElements;
/** Nombre total de pages. */
private final int totalPages;
/**
* Indique s'il existe une page suivante.
*
* @return {@code true} si une page suivante
* existe
*/
public boolean hasNext() {
return page < totalPages - 1;
}
/**
* Indique s'il existe une page précédente.
*
* @return {@code true} si une page précédente
* existe
*/
public boolean hasPrevious() {
return page > 0;
}
}

View File

@@ -0,0 +1,45 @@
package dev.lions.unionflow.server.api.dto.collectefonds;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.collectefonds.StatutCampagneCollecte;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Campagne de levée de fonds (Crowdfunding / ONG)")
public class CampagneCollecteResponse extends BaseDTO {
private String organisationId;
private String titre;
private String courteDescription;
private String htmlDescriptionComplete;
private String imageBanniereUrl;
@Schema(description = "Objectif monétaire escompté")
private BigDecimal objectifFinancier;
@Schema(description = "Somme totale déjà récoltée sur cette campagne")
private BigDecimal montantCollecteActuel;
private Integer nombreDonateurs;
private StatutCampagneCollecte statut;
private LocalDateTime dateOuverture;
private LocalDateTime dateCloturePrevue;
// Si la page est visible pour les non-membres (donateurs externes)
private Boolean estPublique;
}

View File

@@ -0,0 +1,42 @@
package dev.lions.unionflow.server.api.dto.collectefonds;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Don transactionnel reçu pour une campagne de collecte")
public class ContributionCollecteDTO extends BaseDTO {
private String campagneId;
@Schema(description = "Id du membre (Null si le don est public/externe)")
private String membreDonateurId;
@Schema(description = "Nom affiché si don public ou pour le mur de contributeurs")
private String aliasDonateur;
private Boolean estAnonyme;
private BigDecimal montantSoutien;
private String messageSoutien;
private LocalDateTime dateContribution;
// Lien avec la passerelle de paiement
private String transactionPaiementId;
private StatutTransactionWave statutPaiement;
}

View File

@@ -0,0 +1,125 @@
package dev.lions.unionflow.server.api.dto.common;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* DTO générique pour les réponses paginées de l'API.
* Utilisé par tous les endpoints REST qui renvoient des données paginées.
*
* @param <T> Type des éléments dans la liste
* @author UnionFlow Team
* @version 2.0
*/
public class PagedResponse<T> {
@JsonProperty("data")
private List<T> data;
@JsonProperty("total")
private Long total;
@JsonProperty("page")
private Integer page;
@JsonProperty("size")
private Integer size;
@JsonProperty("totalPages")
private Integer totalPages;
// Constructeurs
public PagedResponse() {
}
public PagedResponse(List<T> data, Long total, Integer page, Integer size) {
this.data = data;
this.total = total;
this.page = page;
this.size = size;
this.totalPages = calculateTotalPages(total, size);
}
public PagedResponse(List<T> data, Long total, Integer page, Integer size, Integer totalPages) {
this.data = data;
this.total = total;
this.page = page;
this.size = size;
this.totalPages = totalPages;
}
// Méthode utilitaire pour calculer le nombre de pages
private Integer calculateTotalPages(Long total, Integer size) {
if (size == null || size == 0 || total == null) {
return 0;
}
return (int) Math.ceil((double) total / size);
}
// Getters et setters
public List<T> getData() {
return data;
}
public void setData(List<T> data) {
this.data = data;
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
this.totalPages = calculateTotalPages(total, this.size);
}
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
this.totalPages = calculateTotalPages(this.total, size);
}
public Integer getTotalPages() {
return totalPages;
}
public void setTotalPages(Integer totalPages) {
this.totalPages = totalPages;
}
// Méthodes utilitaires
public boolean hasNext() {
return page != null && totalPages != null && page < totalPages - 1;
}
public boolean hasPrevious() {
return page != null && page > 0;
}
public boolean isEmpty() {
return data == null || data.isEmpty();
}
@Override
public String toString() {
return "PagedResponse{" +
"total=" + total +
", page=" + page +
", size=" + size +
", totalPages=" + totalPages +
", itemsCount=" + (data != null ? data.size() : 0) +
'}';
}
}

View File

@@ -0,0 +1,28 @@
package dev.lions.unionflow.server.api.dto.comptabilite.request;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeCompteComptable;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import lombok.Builder;
/**
* Requête de création d'un compte comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreateCompteComptableRequest(
@NotBlank(message = "Le numéro de compte est obligatoire") String numeroCompte,
@NotBlank(message = "Le libellé est obligatoire") String libelle,
@NotNull(message = "Le type de compte est obligatoire") TypeCompteComptable typeCompte,
@NotNull(message = "La classe comptable est obligatoire") @Min(value = 1, message = "La classe comptable doit être entre 1 et 7") @Max(value = 7, message = "La classe comptable doit être entre 1 et 7") Integer classeComptable,
BigDecimal soldeInitial,
BigDecimal soldeActuel,
Boolean compteCollectif,
Boolean compteAnalytique,
String description) {
}

View File

@@ -0,0 +1,41 @@
package dev.lions.unionflow.server.api.dto.comptabilite.request;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de création d'une écriture comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreateEcritureComptableRequest(
@NotBlank(message = "Le numéro de pièce est obligatoire") String numeroPiece,
@NotNull(message = "La date de l'écriture est obligatoire") LocalDate dateEcriture,
@NotBlank(message = "Le libellé est obligatoire") String libelle,
String reference,
String lettrage,
Boolean pointe,
@DecimalMin(value = "0.0") @Digits(integer = 12, fraction = 2) BigDecimal montantDebit,
@DecimalMin(value = "0.0") @Digits(integer = 12, fraction = 2) BigDecimal montantCredit,
String commentaire,
@NotNull(message = "Le journal est obligatoire") UUID journalId,
UUID organisationId,
UUID paiementId,
List<CreateLigneEcritureRequest> lignes) {
}

View File

@@ -0,0 +1,24 @@
package dev.lions.unionflow.server.api.dto.comptabilite.request;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeJournalComptable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import lombok.Builder;
/**
* Requête de création d'un journal comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreateJournalComptableRequest(
@NotBlank(message = "Le code du journal est obligatoire") String code,
@NotBlank(message = "Le libellé est obligatoire") String libelle,
@NotNull(message = "Le type de journal est obligatoire") TypeJournalComptable typeJournal,
LocalDate dateDebut,
LocalDate dateFin,
String statut,
String description) {
}

View File

@@ -0,0 +1,30 @@
package dev.lions.unionflow.server.api.dto.comptabilite.request;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de création d'une ligne d'écriture comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreateLigneEcritureRequest(
@NotNull(message = "Le numéro de ligne est obligatoire") @Min(value = 1, message = "Le numéro de ligne doit être positif") Integer numeroLigne,
@DecimalMin(value = "0.0", message = "Le montant débit doit être positif ou nul") @Digits(integer = 12, fraction = 2) BigDecimal montantDebit,
@DecimalMin(value = "0.0", message = "Le montant crédit doit être positif ou nul") @Digits(integer = 12, fraction = 2) BigDecimal montantCredit,
String libelle,
String reference,
@NotNull(message = "L'écriture est obligatoire") UUID ecritureId,
@NotNull(message = "Le compte comptable est obligatoire") UUID compteComptableId) {
}

View File

@@ -0,0 +1,26 @@
package dev.lions.unionflow.server.api.dto.comptabilite.request;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeCompteComptable;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import java.math.BigDecimal;
import lombok.Builder;
/**
* Requête de mise à jour d'un compte comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record UpdateCompteComptableRequest(
String numeroCompte,
String libelle,
TypeCompteComptable typeCompte,
@Min(value = 1, message = "La classe comptable doit être entre 1 et 7") @Max(value = 7, message = "La classe comptable doit être entre 1 et 7") Integer classeComptable,
BigDecimal soldeInitial,
BigDecimal soldeActuel,
Boolean compteCollectif,
Boolean compteAnalytique,
String description) {
}

View File

@@ -0,0 +1,34 @@
package dev.lions.unionflow.server.api.dto.comptabilite.request;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de mise à jour d'une écriture comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record UpdateEcritureComptableRequest(
String numeroPiece,
LocalDate dateEcriture,
String libelle,
String reference,
String lettrage,
Boolean pointe,
@DecimalMin(value = "0.0") @Digits(integer = 12, fraction = 2) BigDecimal montantDebit,
@DecimalMin(value = "0.0") @Digits(integer = 12, fraction = 2) BigDecimal montantCredit,
String commentaire,
UUID journalId,
UUID paiementId,
List<UpdateLigneEcritureRequest> lignes) {
}

View File

@@ -0,0 +1,21 @@
package dev.lions.unionflow.server.api.dto.comptabilite.request;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeJournalComptable;
import java.time.LocalDate;
import lombok.Builder;
/**
* Requête de mise à jour d'un journal comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record UpdateJournalComptableRequest(
String libelle,
TypeJournalComptable typeJournal,
LocalDate dateDebut,
LocalDate dateFin,
String statut,
String description) {
}

View File

@@ -0,0 +1,24 @@
package dev.lions.unionflow.server.api.dto.comptabilite.request;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Min;
import java.math.BigDecimal;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de mise à jour d'une ligne d'écriture comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record UpdateLigneEcritureRequest(
@Min(value = 1, message = "Le numéro de ligne doit être positif") Integer numeroLigne,
@DecimalMin(value = "0.0", message = "Le montant débit doit être positif ou nul") @Digits(integer = 12, fraction = 2) BigDecimal montantDebit,
@DecimalMin(value = "0.0", message = "Le montant crédit doit être positif ou nul") @Digits(integer = 12, fraction = 2) BigDecimal montantCredit,
String libelle,
String reference,
UUID compteComptableId) {
}

View File

@@ -0,0 +1,35 @@
package dev.lions.unionflow.server.api.dto.comptabilite.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeCompteComptable;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse détaillée d'un compte comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CompteComptableResponse extends BaseResponse {
private String numeroCompte;
private String libelle;
private TypeCompteComptable typeCompte;
private Integer classeComptable;
private BigDecimal soldeInitial;
private BigDecimal soldeActuel;
private Boolean compteCollectif;
private Boolean compteAnalytique;
private String description;
}

View File

@@ -0,0 +1,41 @@
package dev.lions.unionflow.server.api.dto.comptabilite.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse détaillée d'une écriture comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EcritureComptableResponse extends BaseResponse {
private String numeroPiece;
private LocalDate dateEcriture;
private String libelle;
private String reference;
private String lettrage;
private Boolean pointe;
private BigDecimal montantDebit;
private BigDecimal montantCredit;
private String commentaire;
private UUID journalId;
private UUID organisationId;
private UUID paiementId;
private List<LigneEcritureResponse> lignes;
}

View File

@@ -0,0 +1,33 @@
package dev.lions.unionflow.server.api.dto.comptabilite.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeJournalComptable;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse détaillée d'un journal comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class JournalComptableResponse extends BaseResponse {
private String code;
private String libelle;
private TypeJournalComptable typeJournal;
private LocalDate dateDebut;
private LocalDate dateFin;
private String statut;
private String description;
}

View File

@@ -0,0 +1,33 @@
package dev.lions.unionflow.server.api.dto.comptabilite.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.math.BigDecimal;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse détaillée d'une ligne d'écriture comptable.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LigneEcritureResponse extends BaseResponse {
private Integer numeroLigne;
private BigDecimal montantDebit;
private BigDecimal montantCredit;
private String libelle;
private String reference;
private UUID ecritureId;
private UUID compteComptableId;
}

View File

@@ -0,0 +1,22 @@
package dev.lions.unionflow.server.api.dto.config.request;
import java.util.Map;
import lombok.Builder;
/**
* Requête de création d'une configuration.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreateConfigurationRequest(
String cle,
String valeur,
String type,
String categorie,
String description,
Boolean modifiable,
Boolean visible,
Map<String, Object> metadonnees) {
}

View File

@@ -0,0 +1,50 @@
package dev.lions.unionflow.server.api.dto.config.request;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.math.BigDecimal;
/**
* Requête de création/mise à jour des paramètres LCB-FT (Lutte contre le Blanchiment et le Financement du Terrorisme).
* Définit les seuils au-dessus desquels les justifications d'origine des fonds sont obligatoires.
*
* @author lions dev Team
* @version 1.0
* @since 2026-03-13
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Paramètres LCB-FT (seuils de vigilance)")
public class ParametresLcbFtRequest {
@Schema(description = "ID de l'organisation (null pour paramètres plateforme)")
private String organisationId;
@NotNull(message = "Le montant seuil de justification est obligatoire")
@DecimalMin(value = "0", message = "Le montant doit être positif ou nul")
@Schema(description = "Montant au-dessus duquel l'origine des fonds est obligatoire (ex. 500000 XOF)", example = "500000")
private BigDecimal montantSeuilJustification;
@NotNull(message = "Le montant seuil de validation manuelle est obligatoire")
@DecimalMin(value = "0", message = "Le montant doit être positif ou nul")
@Schema(description = "Montant au-dessus duquel une validation manuelle est requise (ex. 1000000 XOF)", example = "1000000")
private BigDecimal montantSeuilValidationManuelle;
@NotBlank(message = "Le code devise est obligatoire")
@Size(max = 3, message = "Le code devise doit faire 3 caractères (ISO 4217)")
@Schema(description = "Code devise ISO 4217 (ex. XOF, EUR, USD)", example = "XOF")
private String codeDevise;
@Schema(description = "Notes ou commentaires sur la configuration")
private String notes;
}

View File

@@ -0,0 +1,22 @@
package dev.lions.unionflow.server.api.dto.config.request;
import java.util.Map;
import lombok.Builder;
/**
* Requête de mise à jour d'une configuration.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record UpdateConfigurationRequest(
String cle,
String valeur,
String type,
String categorie,
String description,
Boolean modifiable,
Boolean visible,
Map<String, Object> metadonnees) {
}

View File

@@ -0,0 +1,33 @@
package dev.lions.unionflow.server.api.dto.config.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse contenant les données d'une configuration.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ConfigurationResponse extends BaseResponse {
private String cle;
private String valeur;
private String type;
private String categorie;
private String description;
private Boolean modifiable;
private Boolean visible;
private Map<String, Object> metadonnees;
}

View File

@@ -0,0 +1,49 @@
package dev.lions.unionflow.server.api.dto.config.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.math.BigDecimal;
/**
* Réponse contenant les paramètres LCB-FT (Lutte contre le Blanchiment et le Financement du Terrorisme).
* Retourne les seuils configurés pour une organisation ou la plateforme.
*
* @author lions dev Team
* @version 1.0
* @since 2026-03-13
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Paramètres LCB-FT avec seuils de vigilance")
public class ParametresLcbFtResponse extends BaseResponse {
@Schema(description = "ID de l'organisation (null si paramètres plateforme)")
private String organisationId;
@Schema(description = "Nom de l'organisation (null si paramètres plateforme)")
private String organisationNom;
@Schema(description = "Montant au-dessus duquel l'origine des fonds est obligatoire", example = "500000")
private BigDecimal montantSeuilJustification;
@Schema(description = "Montant au-dessus duquel une validation manuelle est requise", example = "1000000")
private BigDecimal montantSeuilValidationManuelle;
@Schema(description = "Code devise ISO 4217", example = "XOF")
private String codeDevise;
@Schema(description = "Notes ou commentaires sur la configuration")
private String notes;
@Schema(description = "Indique si ces paramètres s'appliquent à toute la plateforme")
private Boolean estParametrePlateforme;
}

View File

@@ -0,0 +1,62 @@
package dev.lions.unionflow.server.api.dto.cotisation.request;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de création d'une nouvelle cotisation.
*
* @param membreId Identifiant du membre concerné.
* @param typeCotisation Type de cotisation (MENSUELLE, etc.).
* @param libelle Libellé de la cotisation.
* @param description Description détaillée.
* @param montantDu Montant total à payer.
* @param codeDevise Code ISO de la devise (par défaut XOF).
* @param dateEcheance Date limite de paiement.
* @param periode Période concernée (ex: Janvier 2025).
* @param annee Année de référence.
* @param mois Mois de référence (1-12, optionnel).
* @param recurrente Indique si la cotisation est récurrente.
* @param observations Commentaires libres.
* @author UnionFlow Team
* @version 1.0
* @since 2026-02-22
*/
@Builder
public record CreateCotisationRequest(
@NotNull(message = "L'identifiant du membre est obligatoire") UUID membreId,
@NotNull(message = "L'identifiant de l'organisation est obligatoire") UUID organisationId,
@NotBlank(message = "Le type de cotisation est obligatoire") String typeCotisation,
@NotBlank(message = "Le libellé est obligatoire") @Size(max = 100) String libelle,
@Size(max = 500) String description,
@NotNull(message = "Le montant dû est obligatoire") @DecimalMin(value = "0.0", inclusive = false) @Digits(integer = 10, fraction = 2) BigDecimal montantDu,
@Size(min = 3, max = 3) String codeDevise,
@NotNull(message = "La date d'échéance est obligatoire") LocalDate dateEcheance,
@Size(max = 50) String periode,
@Min(2020) @Max(2100) Integer annee,
@Min(1) @Max(12) Integer mois,
Boolean recurrente,
@Size(max = 1000) String observations) {
}

View File

@@ -0,0 +1,48 @@
package dev.lions.unionflow.server.api.dto.cotisation.request;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
/**
* Requête de mise à jour d'une cotisation existante.
*
* @param libelle Nouveau libellé.
* @param description Nouvelle description.
* @param montantDu Nouveau montant dû (si non payé).
* @param dateEcheance Nouvelle date d'échéance.
* @param observations Nouvelles observations.
* @param statut Nouveau statut (validation métier requise).
* @param annee Année de référence.
* @param mois Mois de référence.
* @param recurrente État de récurrence.
* @author UnionFlow Team
* @version 1.0
* @since 2026-02-22
*/
@Builder
public record UpdateCotisationRequest(
@Size(max = 100) String libelle,
@Size(max = 500) String description,
@DecimalMin(value = "0.0", inclusive = false) @Digits(integer = 10, fraction = 2) BigDecimal montantDu,
LocalDate dateEcheance,
@Size(max = 1000) String observations,
String statut,
@Min(2020) @Max(2100) Integer annee,
@Min(1) @Max(12) Integer mois,
Boolean recurrente) {
}

View File

@@ -0,0 +1,103 @@
package dev.lions.unionflow.server.api.dto.cotisation.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse détaillée pour une cotisation.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-02-22
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CotisationResponse extends BaseResponse {
private String numeroReference;
private UUID membreId;
private String nomMembre;
/** Nom complet (prénom + nom) pour affichage */
private String nomCompletMembre;
private String numeroMembre;
/** Initiales du membre (ex: JD pour Jean Dupont) */
private String initialesMembre;
/** Type / statut du membre (ex: Actif, En attente) */
private String typeMembre;
private UUID organisationId;
private String nomOrganisation;
/** Région de l'organisation (affichage liste) */
private String regionOrganisation;
/** Classe CSS icône PrimeFaces pour l'organisation (ex: pi-building) */
private String iconeOrganisation;
private String typeCotisation;
/** Alias pour tri/filtre (type de cotisation) */
private String type;
private String typeCotisationLibelle;
/** Libellé du type pour affichage (alias typeCotisationLibelle) */
private String typeLibelle;
/** Sévérité PrimeFaces pour le tag type (info, success, warn, error, secondary) */
private String typeSeverity;
/** Classe icône PrimeFaces pour le type (ex: pi-calendar) */
private String typeIcon;
private String libelle;
private String description;
private BigDecimal montantDu;
/** Alias pour tri/filtre (montant du) */
private BigDecimal montant;
/** Montant formaté pour affichage (ex: "5 000") */
private String montantFormatte;
private BigDecimal montantPaye;
private BigDecimal montantRestant;
private String codeDevise;
private String statut;
private String statutLibelle;
/** Sévérité PrimeFaces pour le tag statut */
private String statutSeverity;
/** Classe icône PrimeFaces pour le statut (ex: pi-check) */
private String statutIcon;
private LocalDate dateEcheance;
/** Date d'échéance formatée pour affichage */
private String dateEcheanceFormattee;
/** Classe CSS couleur pour le retard (ex: text-red-500) */
private String retardCouleur;
/** Texte affiché pour le retard (ex: "X jours de retard") */
private String retardTexte;
/** Date de paiement formatée pour affichage */
private String datePaiementFormattee;
/** Icône PrimeFaces pour le mode de paiement */
private String modePaiementIcon;
/** Libellé du mode de paiement */
private String modePaiementLibelle;
private LocalDateTime datePaiement;
private String periode;
private Integer annee;
private Integer mois;
private String observations;
private Boolean recurrente;
private Integer nombreRappels;
private LocalDateTime dateDernierRappel;
private UUID valideParId;
private String nomValidateur;
private LocalDateTime dateValidation;
private Integer pourcentagePaiement;
private Long joursRetard;
private Boolean enRetard;
// Informations de paiement
private String methodePaiement; // WAVE_MONEY, VIREMENT, ESPECES, CARTE, MOBILE_MONEY
private String referencePaiement; // Référence externe du paiement
private String waveSessionId; // ID de session Wave Money pour prélèvements
}

View File

@@ -0,0 +1,35 @@
package dev.lions.unionflow.server.api.dto.cotisation.response;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
/**
* Réponse simplifiée pour les listes de cotisations.
*
* @param id Identifiant unique.
* @param numeroReference Référence unique.
* @param nomMembre Nom du membre.
* @param montantDu Montant total dû.
* @param montantPaye Montant déjà réglé.
* @param statut Statut actuel (code).
* @param statutLibelle Libellé du statut pour l'UI.
* @param dateEcheance Date limite.
* @param annee Année concernée.
* @param actif Indique si l'entité est active.
* @author UnionFlow Team
* @version 1.0
* @since 2026-02-22
*/
public record CotisationSummaryResponse(
UUID id,
String numeroReference,
String nomMembre,
BigDecimal montantDu,
BigDecimal montantPaye,
String statut,
String statutLibelle,
LocalDate dateEcheance,
Integer annee,
Boolean actif) {
}

View File

@@ -0,0 +1,33 @@
package dev.lions.unionflow.server.api.dto.culte;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.culte.TypeDonReligieux;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DonReligieuxDTO extends BaseDTO {
private String institutionId; // Mosquée, Église, Paroisse...
// Si relié spécifiquement à un fidèle enregistré
private String fideleId;
private TypeDonReligieux typeDon;
private BigDecimal montant;
private LocalDateTime dateEncaissement;
// Utile pour la zakat (Nissab de l'année concernée) ou la dîme périodique
private String periodeOuNatureAssociee;
}

View File

@@ -0,0 +1,122 @@
package dev.lions.unionflow.server.api.dto.dashboard;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* DTO principal pour toutes les données du dashboard
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardDataResponse {
@JsonProperty("stats")
private DashboardStatsResponse stats;
@JsonProperty("recentActivities")
private List<RecentActivityResponse> recentActivities;
@JsonProperty("upcomingEvents")
private List<UpcomingEventResponse> upcomingEvents;
@JsonProperty("userPreferences")
private Map<String, Object> userPreferences;
@JsonProperty("organizationId")
private String organizationId;
@JsonProperty("userId")
private String userId;
// Méthodes utilitaires
public Integer getTodayEventsCount() {
if (upcomingEvents == null) {
return 0;
}
return (int) upcomingEvents.stream()
.filter(event -> event.getIsToday() != null && event.getIsToday())
.count();
}
public Integer getTomorrowEventsCount() {
if (upcomingEvents == null) {
return 0;
}
return (int) upcomingEvents.stream()
.filter(event -> event.getIsTomorrow() != null && event.getIsTomorrow())
.count();
}
public Integer getRecentActivitiesCount() {
if (recentActivities == null) {
return 0;
}
return (int) recentActivities.stream()
.filter(activity -> activity.getIsRecent() != null && activity.getIsRecent())
.count();
}
public Integer getTodayActivitiesCount() {
if (recentActivities == null) {
return 0;
}
return (int) recentActivities.stream()
.filter(activity -> activity.getIsToday() != null && activity.getIsToday())
.count();
}
public Boolean getHasUpcomingEvents() {
return upcomingEvents != null && !upcomingEvents.isEmpty();
}
public Boolean getHasRecentActivities() {
return recentActivities != null && !recentActivities.isEmpty();
}
public String getThemePreference() {
if (userPreferences == null) {
return "royal_teal";
}
return (String) userPreferences.getOrDefault("theme", "royal_teal");
}
public String getLanguagePreference() {
if (userPreferences == null) {
return "fr";
}
return (String) userPreferences.getOrDefault("language", "fr");
}
public Boolean getNotificationsEnabled() {
if (userPreferences == null) {
return true;
}
return (Boolean) userPreferences.getOrDefault("notifications", true);
}
public Boolean getAutoRefreshEnabled() {
if (userPreferences == null) {
return true;
}
return (Boolean) userPreferences.getOrDefault("autoRefresh", true);
}
public Integer getRefreshInterval() {
if (userPreferences == null) {
return 300; // 5 minutes par défaut
}
return (Integer) userPreferences.getOrDefault("refreshInterval", 300);
}
}

View File

@@ -0,0 +1,119 @@
package dev.lions.unionflow.server.api.dto.dashboard;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* DTO pour les statistiques du dashboard
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DashboardStatsResponse {
@JsonProperty("totalMembers")
private Integer totalMembers;
@JsonProperty("activeMembers")
private Integer activeMembers;
@JsonProperty("totalEvents")
private Integer totalEvents;
@JsonProperty("upcomingEvents")
private Integer upcomingEvents;
@JsonProperty("totalContributions")
private Integer totalContributions;
@JsonProperty("totalContributionAmount")
private Double totalContributionAmount;
@JsonProperty("pendingRequests")
private Integer pendingRequests;
@JsonProperty("completedProjects")
private Integer completedProjects;
@JsonProperty("monthlyGrowth")
private Double monthlyGrowth;
@JsonProperty("engagementRate")
private Double engagementRate;
@JsonProperty("lastUpdated")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime lastUpdated;
/**
* Nombre total d'organisations dans le système (SuperAdmin uniquement)
*/
@JsonProperty("totalOrganizations")
private Integer totalOrganizations;
/**
* Répartition des organisations par type
* Exemple: {"Mutuelle": 15, "Coopérative": 8, "Tontine": 5, "Autre": 3}
*/
@JsonProperty("organizationTypeDistribution")
private Map<String, Integer> organizationTypeDistribution;
/**
* Données historiques mensuelles pour les graphiques (12 derniers mois)
*/
@JsonProperty("monthlyHistoricalData")
private List<MonthlyStatDTO> monthlyHistoricalData;
// Méthodes utilitaires
public String getFormattedContributionAmount() {
if (totalContributionAmount == null) {
return "0";
}
if (totalContributionAmount >= 1_000_000) {
return String.format("%.1fM", totalContributionAmount / 1_000_000);
} else if (totalContributionAmount >= 1_000) {
return String.format("%.0fK", totalContributionAmount / 1_000);
} else {
return String.format("%.0f", totalContributionAmount);
}
}
public Boolean getHasGrowth() {
return monthlyGrowth != null && monthlyGrowth > 0;
}
public Boolean getIsHighEngagement() {
return engagementRate != null && engagementRate > 0.7;
}
public Double getInactiveMembers() {
if (totalMembers == null || activeMembers == null) {
return 0.0;
}
return (double) (totalMembers - activeMembers);
}
public Double getActiveMemberPercentage() {
if (totalMembers == null || activeMembers == null || totalMembers == 0) {
return 0.0;
}
return (double) activeMembers / totalMembers * 100;
}
public Double getEngagementPercentage() {
if (engagementRate == null) {
return 0.0;
}
return engagementRate * 100;
}
}

View File

@@ -0,0 +1,48 @@
package dev.lions.unionflow.server.api.dto.dashboard;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* DTO de synthèse dashboard membre (cotisations, épargne, événements, aides).
*
* @author UnionFlow Team
* @version 1.0
*/
public record MembreDashboardSyntheseResponse(
String prenom,
String nom,
LocalDate dateInscription,
// Cotisations
BigDecimal mesCotisationsPaiement,
/** Total des cotisations payées sur l'année en cours (pour affichage dashboard). */
BigDecimal totalCotisationsPayeesAnnee,
/** Total des cotisations payées tout temps (pour la carte « Contribution Totale »). */
BigDecimal totalCotisationsPayeesToutTemps,
/** Nombre de cotisations PAYÉES (pour la carte « Cotisations »). */
Integer nombreCotisationsPayees,
String statutCotisations,
/** Taux de cotisation en % (0-100). Calculé sur l'année courante ou toutes années si pas de cotisation 2026. */
Integer tauxCotisationsPerso,
/** Nombre TOTAL de cotisations (toutes années, tous statuts) — pour calcul du taux d'engagement. */
Integer nombreCotisationsTotal,
// Epargne
BigDecimal monSoldeEpargne,
BigDecimal evolutionEpargneNombre,
String evolutionEpargne,
Integer objectifEpargne,
// Evenements
Integer mesEvenementsInscrits,
Integer evenementsAVenir,
Integer tauxParticipationPerso,
// Aides
Integer mesDemandesAide,
Integer aidesEnCours,
Integer tauxAidesApprouvees) implements Serializable {
}

View File

@@ -0,0 +1,70 @@
package dev.lions.unionflow.server.api.dto.dashboard;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO pour les statistiques mensuelles historiques
* Utilisé pour générer des graphiques de croissance sur 12 mois
*
* @author UnionFlow Team
* @version 2.0
* @since 2026-03-07
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MonthlyStatDTO {
/**
* Mois au format "2026-01", "2026-02", etc.
*/
@JsonProperty("month")
private String month;
/**
* Nombre de membres ce mois-là
*/
@JsonProperty("totalMembers")
private Integer totalMembers;
/**
* Nombre de membres actifs ce mois-là
*/
@JsonProperty("activeMembers")
private Integer activeMembers;
/**
* Montant total des contributions ce mois-là
*/
@JsonProperty("contributionAmount")
private Double contributionAmount;
/**
* Nombre d'événements organisés ce mois-là
*/
@JsonProperty("eventsCount")
private Integer eventsCount;
/**
* Taux d'engagement ce mois-là
*/
@JsonProperty("engagementRate")
private Double engagementRate;
/**
* Nombre de nouveaux membres ce mois-là
*/
@JsonProperty("newMembers")
private Integer newMembers;
/**
* Nombre de cotisations payées ce mois-là
*/
@JsonProperty("contributionsCount")
private Integer contributionsCount;
}

View File

@@ -0,0 +1,130 @@
package dev.lions.unionflow.server.api.dto.dashboard;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
/**
* DTO pour les activités récentes du dashboard
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RecentActivityResponse {
@JsonProperty("id")
private String id;
@JsonProperty("type")
private String type;
@JsonProperty("title")
private String title;
@JsonProperty("description")
private String description;
@JsonProperty("userName")
private String userName;
@JsonProperty("timestamp")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime timestamp;
@JsonProperty("userAvatar")
private String userAvatar;
@JsonProperty("actionUrl")
private String actionUrl;
// Méthodes utilitaires
public String getTimeAgo() {
if (timestamp == null) {
return "";
}
LocalDateTime now = LocalDateTime.now();
long minutes = ChronoUnit.MINUTES.between(timestamp, now);
long hours = ChronoUnit.HOURS.between(timestamp, now);
long days = ChronoUnit.DAYS.between(timestamp, now);
if (minutes < 60) {
return minutes + "min";
} else if (hours < 24) {
return hours + "h";
} else if (days < 7) {
return days + "j";
} else {
long weeks = days / 7;
return weeks + "sem";
}
}
public String getActivityIcon() {
if (type == null) {
return "help_outline";
}
switch (type.toLowerCase()) {
case "member":
return "person";
case "event":
return "event";
case "contribution":
return "payment";
case "organization":
return "business";
case "system":
return "settings";
default:
return "info";
}
}
public String getActivityColor() {
if (type == null) {
return "#6B7280"; // grey
}
switch (type.toLowerCase()) {
case "member":
return "#10B981"; // success
case "event":
return "#3B82F6"; // info
case "contribution":
return "#008B8B"; // teal blue
case "organization":
return "#4169E1"; // royal blue
case "system":
return "#6B7280"; // grey
default:
return "#6B7280"; // grey
}
}
public Boolean getIsRecent() {
if (timestamp == null) {
return false;
}
LocalDateTime now = LocalDateTime.now();
long hours = ChronoUnit.HOURS.between(timestamp, now);
return hours < 24;
}
public Boolean getIsToday() {
if (timestamp == null) {
return false;
}
LocalDateTime now = LocalDateTime.now();
return timestamp.toLocalDate().equals(now.toLocalDate());
}
}

View File

@@ -0,0 +1,183 @@
package dev.lions.unionflow.server.api.dto.dashboard;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
/**
* DTO pour les événements à venir du dashboard
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpcomingEventResponse {
@JsonProperty("id")
private String id;
@JsonProperty("title")
private String title;
@JsonProperty("description")
private String description;
@JsonProperty("startDate")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime startDate;
@JsonProperty("endDate")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime endDate;
@JsonProperty("location")
private String location;
@JsonProperty("maxParticipants")
private Integer maxParticipants;
@JsonProperty("currentParticipants")
private Integer currentParticipants;
@JsonProperty("status")
private String status;
@JsonProperty("imageUrl")
private String imageUrl;
@JsonProperty("tags")
private List<String> tags;
// Méthodes utilitaires
public String getDaysUntilEvent() {
return getDaysUntilEvent(LocalDateTime.now());
}
/**
* Version testable avec une date de référence fixe (même package).
*/
String getDaysUntilEvent(LocalDateTime now) {
if (startDate == null) {
return "";
}
long days = ChronoUnit.DAYS.between(now.toLocalDate(), startDate.toLocalDate());
long hours = ChronoUnit.HOURS.between(now, startDate);
if (days < 0) {
return "En cours";
}
if (days == 0) {
// Vérifier si l'événement est déjà passé (même si moins d'1h)
if (startDate.isBefore(now)) {
return "En cours";
} else if (hours < 2) {
return "Bientôt";
} else {
return "Aujourd'hui";
}
} else if (days == 1) {
return "Demain";
} else if (days < 7) {
return "Dans " + days + " jours";
} else {
long weeks = days / 7;
return "Dans " + weeks + " semaine" + (weeks > 1 ? "s" : "");
}
}
public Double getFillPercentage() {
if (maxParticipants == null || currentParticipants == null || maxParticipants == 0) {
return 0.0;
}
return (double) currentParticipants / maxParticipants * 100;
}
public Boolean getIsFull() {
if (maxParticipants == null || currentParticipants == null) {
return false;
}
return currentParticipants >= maxParticipants;
}
public Boolean getIsAlmostFull() {
Double fillPercentage = getFillPercentage();
return fillPercentage >= 80.0 && fillPercentage < 100.0;
}
public Boolean getIsToday() {
if (startDate == null) {
return false;
}
LocalDateTime now = LocalDateTime.now();
return startDate.toLocalDate().equals(now.toLocalDate());
}
public Boolean getIsTomorrow() {
if (startDate == null) {
return false;
}
LocalDateTime now = LocalDateTime.now();
return startDate.toLocalDate().equals(now.toLocalDate().plusDays(1));
}
public String getStatusColor() {
if (status == null) {
return "#6B7280"; // grey
}
switch (status.toLowerCase()) {
case "confirmed":
return "#10B981"; // success
case "open":
return "#3B82F6"; // info
case "cancelled":
return "#EF4444"; // error
case "postponed":
return "#F59E0B"; // warning
default:
return "#6B7280"; // grey
}
}
public String getStatusLabel() {
if (status == null) {
return "Inconnu";
}
switch (status.toLowerCase()) {
case "confirmed":
return "Confirmé";
case "open":
return "Ouvert";
case "cancelled":
return "Annulé";
case "postponed":
return "Reporté";
default:
return status;
}
}
public Integer getAvailableSpots() {
if (maxParticipants == null || currentParticipants == null) {
return 0;
}
return Math.max(0, maxParticipants - currentParticipants);
}
public String getParticipationSummary() {
if (maxParticipants == null || currentParticipants == null) {
return "0/0 participants";
}
return currentParticipants + "/" + maxParticipants + " participants";
}
}

View File

@@ -0,0 +1,26 @@
package dev.lions.unionflow.server.api.dto.document.request;
import dev.lions.unionflow.server.api.enums.document.TypeDocument;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
/**
* Requête de création d'un document.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreateDocumentRequest(
@NotBlank(message = "Le nom du fichier est obligatoire") String nomFichier,
String nomOriginal,
@NotBlank(message = "Le chemin de stockage est obligatoire") String cheminStockage,
String typeMime,
@NotNull(message = "La taille est obligatoire") @Min(value = 0, message = "La taille doit être positive") Long tailleOctets,
TypeDocument typeDocument,
String hashMd5,
String hashSha256,
String description) {
}

View File

@@ -0,0 +1,22 @@
package dev.lions.unionflow.server.api.dto.document.request;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de création d'une pièce jointe.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreatePieceJointeRequest(
@NotNull(message = "L'ordre est obligatoire") @Min(value = 1, message = "L'ordre doit être positif") Integer ordre,
String libelle,
String commentaire,
@NotNull(message = "Le document est obligatoire") UUID documentId,
@NotNull(message = "Le type entité est obligatoire") String typeEntiteRattachee,
@NotNull(message = "L'ID entité est obligatoire") UUID entiteRattacheeId) {
}

View File

@@ -0,0 +1,18 @@
package dev.lions.unionflow.server.api.dto.document.request;
import dev.lions.unionflow.server.api.enums.document.TypeDocument;
import lombok.Builder;
/**
* Requête de mise à jour d'un document.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record UpdateDocumentRequest(
String nomFichier,
String nomOriginal,
TypeDocument typeDocument,
String description) {
}

View File

@@ -0,0 +1,17 @@
package dev.lions.unionflow.server.api.dto.document.request;
import jakarta.validation.constraints.Min;
import lombok.Builder;
/**
* Requête de mise à jour d'une pièce jointe.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record UpdatePieceJointeRequest(
@Min(value = 1, message = "L'ordre doit être positif") Integer ordre,
String libelle,
String commentaire) {
}

View File

@@ -0,0 +1,36 @@
package dev.lions.unionflow.server.api.dto.document.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import dev.lions.unionflow.server.api.enums.document.TypeDocument;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse détaillée d'un document.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DocumentResponse extends BaseResponse {
private String nomFichier;
private String nomOriginal;
private String cheminStockage;
private String typeMime;
private Long tailleOctets;
private TypeDocument typeDocument;
private String hashMd5;
private String hashSha256;
private String description;
private Integer nombreTelechargements;
private String tailleFormatee;
}

View File

@@ -0,0 +1,31 @@
package dev.lions.unionflow.server.api.dto.document.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse contenant les données d'une pièce jointe.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PieceJointeResponse extends BaseResponse {
private Integer ordre;
private String libelle;
private String commentaire;
private UUID documentId;
private String typeEntiteRattachee;
private UUID entiteRattacheeId;
}

View File

@@ -0,0 +1,70 @@
package dev.lions.unionflow.server.api.dto.evenement.request;
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;
import dev.lions.unionflow.server.api.validation.ValidationConstants;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.FutureOrPresent;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de création d'un événement.
*/
@Builder
public record CreateEvenementRequest(
@NotBlank(message = "Le titre"
+ ValidationConstants.OBLIGATOIRE_MESSAGE) @Size(min = ValidationConstants.TITRE_MIN_LENGTH, max = ValidationConstants.TITRE_MAX_LENGTH, message = ValidationConstants.TITRE_SIZE_MESSAGE) String titre,
@Size(max = ValidationConstants.DESCRIPTION_COURTE_MAX_LENGTH, message = ValidationConstants.DESCRIPTION_COURTE_SIZE_MESSAGE) String description,
@NotNull(message = "Le type d'événement est obligatoire") TypeEvenementMetier typeEvenement,
@NotNull(message = "Le statut est obligatoire") StatutEvenement statut,
PrioriteEvenement priorite,
@NotNull(message = "La date de début est obligatoire") @FutureOrPresent(message = "La date de début ne peut pas être dans le passé") LocalDate dateDebut,
LocalDate dateFin,
LocalTime heureDebut,
LocalTime heureFin,
@NotBlank(message = "Le lieu est obligatoire") @Size(max = 100) String lieu,
@Size(max = 200) String adresse,
@Size(max = 50) String ville,
@Size(max = 50) String region,
@DecimalMin(value = "-90.0") @DecimalMax(value = "90.0") BigDecimal latitude,
@DecimalMin(value = "-180.0") @DecimalMax(value = "180.0") BigDecimal longitude,
@NotNull(message = "L'association organisatrice est obligatoire") UUID associationId,
@Size(max = 100) String organisateur,
@Email(message = "Format d'email invalide") @Size(max = 100) String emailOrganisateur,
@Pattern(regexp = "^\\+?[0-9\\s\\-\\(\\)]{8,20}$") String telephoneOrganisateur,
@Min(value = 1) @Max(value = 10000) Integer capaciteMax,
@DecimalMin(value = ValidationConstants.MONTANT_MIN_VALUE, message = ValidationConstants.MONTANT_POSITIF_MESSAGE) @Digits(integer = ValidationConstants.MONTANT_INTEGER_DIGITS, fraction = ValidationConstants.MONTANT_FRACTION_DIGITS, message = ValidationConstants.MONTANT_DIGITS_MESSAGE) BigDecimal budget,
@DecimalMin(value = ValidationConstants.MONTANT_MIN_VALUE, message = ValidationConstants.MONTANT_POSITIF_MESSAGE) @Digits(integer = ValidationConstants.MONTANT_INTEGER_DIGITS, fraction = ValidationConstants.MONTANT_FRACTION_DIGITS, message = ValidationConstants.MONTANT_DIGITS_MESSAGE) BigDecimal coutReel,
@Pattern(regexp = ValidationConstants.DEVISE_PATTERN, message = ValidationConstants.DEVISE_MESSAGE) String codeDevise,
Boolean inscriptionObligatoire,
LocalDate dateLimiteInscription,
Boolean evenementPublic,
Boolean recurrent,
String frequenceRecurrence,
@Size(max = 500) String instructions,
@Size(max = 500) String materielNecessaire,
@Size(max = 100) String conditionsMeteo,
@Size(max = 255) String imageUrl,
@Pattern(regexp = "^#[0-9A-Fa-f]{6}$") String couleurTheme) {
}

View File

@@ -0,0 +1,65 @@
package dev.lions.unionflow.server.api.dto.evenement.request;
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;
import dev.lions.unionflow.server.api.validation.ValidationConstants;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
import lombok.Builder;
/**
* Requête de mise à jour d'un événement.
*/
@Builder
public record UpdateEvenementRequest(
@Size(min = ValidationConstants.TITRE_MIN_LENGTH, max = ValidationConstants.TITRE_MAX_LENGTH, message = ValidationConstants.TITRE_SIZE_MESSAGE) String titre,
@Size(max = ValidationConstants.DESCRIPTION_COURTE_MAX_LENGTH, message = ValidationConstants.DESCRIPTION_COURTE_SIZE_MESSAGE) String description,
TypeEvenementMetier typeEvenement,
StatutEvenement statut,
PrioriteEvenement priorite,
LocalDate dateDebut,
LocalDate dateFin,
LocalTime heureDebut,
LocalTime heureFin,
@NotBlank(message = "Le lieu est obligatoire") @Size(max = 100) String lieu,
@Size(max = 200) String adresse,
@Size(max = 50) String ville,
@Size(max = 50) String region,
@DecimalMin(value = "-90.0") @DecimalMax(value = "90.0") BigDecimal latitude,
@DecimalMin(value = "-180.0") @DecimalMax(value = "180.0") BigDecimal longitude,
@Size(max = 100) String organisateur,
@Email(message = "Format d'email invalide") @Size(max = 100) String emailOrganisateur,
@Pattern(regexp = "^\\+?[0-9\\s\\-\\(\\)]{8,20}$") String telephoneOrganisateur,
@Min(value = 1) @Max(value = 10000) Integer capaciteMax,
@DecimalMin(value = ValidationConstants.MONTANT_MIN_VALUE, message = ValidationConstants.MONTANT_POSITIF_MESSAGE) @Digits(integer = ValidationConstants.MONTANT_INTEGER_DIGITS, fraction = ValidationConstants.MONTANT_FRACTION_DIGITS, message = ValidationConstants.MONTANT_DIGITS_MESSAGE) BigDecimal budget,
@DecimalMin(value = ValidationConstants.MONTANT_MIN_VALUE, message = ValidationConstants.MONTANT_POSITIF_MESSAGE) @Digits(integer = ValidationConstants.MONTANT_INTEGER_DIGITS, fraction = ValidationConstants.MONTANT_FRACTION_DIGITS, message = ValidationConstants.MONTANT_DIGITS_MESSAGE) BigDecimal coutReel,
@Pattern(regexp = ValidationConstants.DEVISE_PATTERN, message = ValidationConstants.DEVISE_MESSAGE) String codeDevise,
Boolean inscriptionObligatoire,
LocalDate dateLimiteInscription,
Boolean evenementPublic,
Boolean recurrent,
String frequenceRecurrence,
@Size(max = 500) String instructions,
@Size(max = 500) String materielNecessaire,
@Size(max = 100) String conditionsMeteo,
@Size(max = 255) String imageUrl,
@Pattern(regexp = "^#[0-9A-Fa-f]{6}$") String couleurTheme) {
}

View File

@@ -0,0 +1,204 @@
package dev.lions.unionflow.server.api.dto.evenement.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
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;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse détaillée pour un événement.
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EvenementResponse extends BaseResponse {
private String titre;
private String description;
private TypeEvenementMetier typeEvenement;
private StatutEvenement statut;
private PrioriteEvenement priorite;
// Décommenter si l'on a besoin du @JsonFormat, sinon on garde le standard
// @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate dateDebut;
// @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate dateFin;
// @JsonFormat(pattern = "HH:mm")
private LocalTime heureDebut;
// @JsonFormat(pattern = "HH:mm")
private LocalTime heureFin;
private String lieu;
private String adresse;
private String ville;
private String region;
private BigDecimal latitude;
private BigDecimal longitude;
private UUID associationId;
private String nomAssociation;
private String organisateur;
private String emailOrganisateur;
private String telephoneOrganisateur;
private Integer capaciteMax;
private Integer participantsInscrits;
private Integer participantsPresents;
private BigDecimal budget;
private BigDecimal coutReel;
private String codeDevise;
private Boolean inscriptionObligatoire;
// @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate dateLimiteInscription;
private Boolean evenementPublic;
private Boolean recurrent;
private String frequenceRecurrence;
private String instructions;
private String materielNecessaire;
private String conditionsMeteo;
private String imageUrl;
private String couleurTheme;
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateAnnulation;
private String raisonAnnulation;
private String nomAnnulateur;
private Long annulePar;
// === METHODES UTILITAIRES ===
public boolean estEnCours() {
return StatutEvenement.EN_COURS.equals(statut);
}
public boolean estTermine() {
return StatutEvenement.TERMINE.equals(statut);
}
public boolean estAnnule() {
return StatutEvenement.ANNULE.equals(statut);
}
public boolean estComplet() {
return capaciteMax != null
&& participantsInscrits != null
&& participantsInscrits >= capaciteMax;
}
public int getPlacesDisponibles() {
if (capaciteMax == null || participantsInscrits == null) {
return 0;
}
return Math.max(0, capaciteMax - participantsInscrits);
}
public int getTauxRemplissage() {
if (capaciteMax == null || capaciteMax == 0 || participantsInscrits == null) {
return 0;
}
return (participantsInscrits * 100) / capaciteMax;
}
public int getTauxPresence() {
if (participantsInscrits == null || participantsInscrits == 0 || participantsPresents == null) {
return 0;
}
return (participantsPresents * 100) / participantsInscrits;
}
public boolean sontInscriptionsOuvertes() {
if (estAnnule() || estTermine()) {
return false;
}
if (dateLimiteInscription != null && LocalDate.now().isAfter(dateLimiteInscription)) {
return false;
}
return !estComplet();
}
public long getDureeEnHeures() {
if (heureDebut == null || heureFin == null) {
return 0;
}
return heureDebut.until(heureFin, java.time.temporal.ChronoUnit.HOURS);
}
public boolean estEvenementMultiJours() {
return dateFin != null && !dateDebut.equals(dateFin);
}
public String getTypeEvenementLibelle() {
return typeEvenement != null ? typeEvenement.getLibelle() : "Non défini";
}
public String getStatutLibelle() {
return statut != null ? statut.getLibelle() : "Non défini";
}
public String getPrioriteLibelle() {
return priorite != null ? priorite.getLibelle() : "Normale";
}
public String getAdresseComplete() {
StringBuilder adresseComplete = new StringBuilder();
if (lieu != null && !lieu.trim().isEmpty()) {
adresseComplete.append(lieu);
}
if (adresse != null && !adresse.trim().isEmpty()) {
if (adresseComplete.length() > 0)
adresseComplete.append(", ");
adresseComplete.append(adresse);
}
if (ville != null && !ville.trim().isEmpty()) {
if (adresseComplete.length() > 0)
adresseComplete.append(", ");
adresseComplete.append(ville);
}
if (region != null && !region.trim().isEmpty()) {
if (adresseComplete.length() > 0)
adresseComplete.append(", ");
adresseComplete.append(region);
}
return adresseComplete.toString();
}
public boolean hasCoordonnees() {
return latitude != null && longitude != null;
}
public BigDecimal getEcartBudgetaire() {
if (budget == null || coutReel == null) {
return BigDecimal.ZERO;
}
return budget.subtract(coutReel);
}
public boolean estBudgetDepasse() {
return getEcartBudgetaire().compareTo(BigDecimal.ZERO) < 0;
}
}

View File

@@ -0,0 +1,26 @@
package dev.lions.unionflow.server.api.dto.favoris.request;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de création d'un favori.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreateFavoriRequest(
UUID utilisateurId,
String typeFavori,
String titre,
String description,
String url,
String icon,
String couleur,
String categorie,
Integer ordre,
Integer nbVisites,
String derniereVisite,
Boolean estPlusUtilise) {
}

View File

@@ -0,0 +1,37 @@
package dev.lions.unionflow.server.api.dto.favoris.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
/**
* Réponse pour un favori utilisateur.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FavoriResponse extends BaseResponse {
private UUID utilisateurId;
private String typeFavori;
private String titre;
private String description;
private String url;
private String icon;
private String couleur;
private String categorie;
private Integer ordre;
private Integer nbVisites;
private String derniereVisite;
private Boolean estPlusUtilise;
}

View File

@@ -0,0 +1,33 @@
package dev.lions.unionflow.server.api.dto.finance.request;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;
import lombok.Builder;
/**
* Requête de création d'une adhésion.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record CreateAdhesionRequest(
@NotBlank(message = "Le numéro de référence est obligatoire") @Size(max = 50, message = "Le numéro de référence ne peut pas dépasser 50 caractères") String numeroReference,
@NotNull(message = "L'identifiant du membre est obligatoire") UUID membreId,
@NotNull(message = "L'identifiant de l'organisation est obligatoire") UUID organisationId,
@NotNull(message = "La date de demande est obligatoire") LocalDate dateDemande,
@NotNull(message = "Les frais d'adhésion sont obligatoires") @DecimalMin(value = "0.0", inclusive = false, message = "Les frais d'adhésion doivent être positifs") @Digits(integer = 10, fraction = 2, message = "Format de montant invalide") BigDecimal fraisAdhesion,
@NotBlank(message = "Le code devise est obligatoire") @Pattern(regexp = "^[A-Z]{3}$", message = "Le code devise doit être un code ISO à 3 lettres") String codeDevise,
@Size(max = 1000, message = "Les observations ne peuvent pas dépasser 1000 caractères") String observations) {
}

View File

@@ -0,0 +1,38 @@
package dev.lions.unionflow.server.api.dto.finance.request;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.Builder;
/**
* Requête de mise à jour d'une adhésion.
*
* @author UnionFlow Team
* @version 1.0
*/
@Builder
public record UpdateAdhesionRequest(
@DecimalMin(value = "0.0", message = "Le montant payé ne peut pas être négatif") @Digits(integer = 10, fraction = 2, message = "Format de montant invalide") BigDecimal montantPaye,
@Pattern(regexp = "^(EN_ATTENTE|APPROUVEE|REJETEE|ANNULEE|EN_PAIEMENT|PAYEE)$", message = "Statut invalide") String statut,
LocalDate dateApprobation,
LocalDateTime datePaiement,
@Pattern(regexp = "^(ESPECES|VIREMENT|CHEQUE|WAVE_MONEY|ORANGE_MONEY|FREE_MONEY|CARTE_BANCAIRE)$", message = "Méthode de paiement invalide") String methodePaiement,
@Size(max = 100, message = "La référence de paiement ne peut pas dépasser 100 caractères") String referencePaiement,
@Size(max = 1000, message = "Le motif de rejet ne peut pas dépasser 1000 caractères") String motifRejet,
@Size(max = 1000, message = "Les observations ne peuvent pas dépasser 1000 caractères") String observations,
@Size(max = 255, message = "Le nom de l'approbateur ne peut pas dépasser 255 caractères") String approuvePar,
LocalDate dateValidation) {
}

View File

@@ -0,0 +1,171 @@
package dev.lions.unionflow.server.api.dto.finance.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse contenant les données d'une adhésion.
*
* @author UnionFlow Team
* @version 1.0
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AdhesionResponse extends BaseResponse {
private String numeroReference;
private UUID membreId;
private String numeroMembre;
private String nomMembre;
private String emailMembre;
private UUID organisationId;
private String nomOrganisation;
private LocalDate dateDemande;
private BigDecimal fraisAdhesion;
private BigDecimal montantPaye;
private String codeDevise;
private String statut;
private LocalDate dateApprobation;
private LocalDateTime datePaiement;
private String methodePaiement;
private String referencePaiement;
private String motifRejet;
private String observations;
private String approuvePar;
private LocalDate dateValidation;
// Méthodes utilitaires héritées de l'ancien DTO
public boolean isPayeeIntegralement() {
return montantPaye != null && fraisAdhesion != null && montantPaye.compareTo(fraisAdhesion) >= 0;
}
public boolean isEnAttentePaiement() {
return "APPROUVEE".equals(statut) && !isPayeeIntegralement();
}
public BigDecimal getMontantRestant() {
if (fraisAdhesion == null)
return BigDecimal.ZERO;
if (montantPaye == null)
return fraisAdhesion;
BigDecimal restant = fraisAdhesion.subtract(montantPaye);
return restant.compareTo(BigDecimal.ZERO) > 0 ? restant : BigDecimal.ZERO;
}
public int getPourcentagePaiement() {
if (fraisAdhesion == null || fraisAdhesion.compareTo(BigDecimal.ZERO) == 0)
return 0;
if (montantPaye == null)
return 0;
return montantPaye.multiply(BigDecimal.valueOf(100)).divide(fraisAdhesion, 0, java.math.RoundingMode.HALF_UP)
.intValue();
}
public long getJoursDepuisDemande() {
if (dateDemande == null)
return 0;
return ChronoUnit.DAYS.between(dateDemande, LocalDate.now());
}
public String getStatutLibelle() {
if (statut == null)
return "Non défini";
return switch (statut) {
case "EN_ATTENTE" -> "En attente";
case "APPROUVEE" -> "Approuvée";
case "REJETEE" -> "Rejetée";
case "ANNULEE" -> "Annulée";
case "EN_PAIEMENT" -> "En paiement";
case "PAYEE" -> "Payée";
default -> statut;
};
}
public String getStatutSeverity() {
if (statut == null)
return "secondary";
return switch (statut) {
case "APPROUVEE", "PAYEE" -> "success";
case "EN_ATTENTE", "EN_PAIEMENT" -> "warning";
case "REJETEE" -> "danger";
case "ANNULEE" -> "secondary";
default -> "secondary";
};
}
public String getStatutIcon() {
if (statut == null)
return "pi-circle";
return switch (statut) {
case "APPROUVEE", "PAYEE" -> "pi-check";
case "EN_ATTENTE" -> "pi-clock";
case "EN_PAIEMENT" -> "pi-credit-card";
case "REJETEE" -> "pi-times";
case "ANNULEE" -> "pi-ban";
default -> "pi-circle";
};
}
public String getMethodePaiementLibelle() {
if (methodePaiement == null)
return "Non défini";
return switch (methodePaiement) {
case "ESPECES" -> "Espèces";
case "VIREMENT" -> "Virement bancaire";
case "CHEQUE" -> "Chèque";
case "WAVE_MONEY" -> "Wave Money";
case "ORANGE_MONEY" -> "Orange Money";
case "FREE_MONEY" -> "Free Money";
case "CARTE_BANCAIRE" -> "Carte bancaire";
default -> methodePaiement;
};
}
public String getDateDemandeFormatee() {
if (dateDemande == null)
return "";
return dateDemande.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
}
public String getDateApprobationFormatee() {
if (dateApprobation == null)
return "";
return dateApprobation.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
}
public String getDatePaiementFormatee() {
if (datePaiement == null)
return "";
return datePaiement.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"));
}
public String getFraisAdhesionFormatte() {
if (fraisAdhesion == null)
return "0 FCFA";
return String.format("%,.0f FCFA", fraisAdhesion.doubleValue());
}
public String getMontantPayeFormatte() {
if (montantPaye == null)
return "0 FCFA";
return String.format("%,.0f FCFA", montantPaye.doubleValue());
}
public String getMontantRestantFormatte() {
return String.format("%,.0f FCFA", getMontantRestant().doubleValue());
}
}

View File

@@ -0,0 +1,24 @@
package dev.lions.unionflow.server.api.dto.finance_workflow.request;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO de requête pour approuver une transaction
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-13
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ApproveTransactionRequest {
@Size(max = 1000, message = "Le commentaire ne peut pas dépasser 1000 caractères")
private String comment;
}

View File

@@ -0,0 +1,42 @@
package dev.lions.unionflow.server.api.dto.finance_workflow.request;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO de requête pour créer une ligne budgétaire
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-13
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CreateBudgetLineRequest {
@NotBlank(message = "La catégorie est requise")
@Pattern(regexp = "^(CONTRIBUTIONS|SAVINGS|SOLIDARITY|EVENTS|OPERATIONAL|INVESTMENTS|OTHER)$",
message = "Catégorie invalide")
private String category;
@NotBlank(message = "Le nom est requis")
@Size(max = 200, message = "Le nom ne peut pas dépasser 200 caractères")
private String name;
@Size(max = 500, message = "La description ne peut pas dépasser 500 caractères")
private String description;
@NotNull(message = "Le montant prévu est requis")
@DecimalMin(value = "0.0", message = "Le montant prévu doit être positif")
@Digits(integer = 14, fraction = 2, message = "Format du montant invalide")
private BigDecimal amountPlanned;
@Size(max = 1000, message = "Les notes ne peuvent pas dépasser 1000 caractères")
private String notes;
}

View File

@@ -0,0 +1,58 @@
package dev.lions.unionflow.server.api.dto.finance_workflow.request;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO de requête pour créer un budget
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-13
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CreateBudgetRequest {
@NotBlank(message = "Le nom est requis")
@Size(max = 200, message = "Le nom ne peut pas dépasser 200 caractères")
private String name;
@Size(max = 1000, message = "La description ne peut pas dépasser 1000 caractères")
private String description;
@NotNull(message = "L'ID de l'organisation est requis")
private UUID organizationId;
@NotBlank(message = "La période est requise")
@Pattern(regexp = "^(MONTHLY|QUARTERLY|SEMIANNUAL|ANNUAL)$",
message = "Période invalide")
private String period;
@NotNull(message = "L'année est requise")
@Min(value = 2020, message = "L'année doit être >= 2020")
@Max(value = 2100, message = "L'année doit être <= 2100")
private Integer year;
@Min(value = 1, message = "Le mois doit être entre 1 et 12")
@Max(value = 12, message = "Le mois doit être entre 1 et 12")
private Integer month;
@NotNull(message = "Au moins une ligne budgétaire est requise")
@Size(min = 1, message = "Au moins une ligne budgétaire est requise")
@Valid
@Builder.Default
private List<CreateBudgetLineRequest> lines = new ArrayList<>();
@Pattern(regexp = "^[A-Z]{3}$", message = "Code devise invalide (doit être ISO 3 lettres)")
private String currency; // Optionnel, défaut XOF
}

View File

@@ -0,0 +1,26 @@
package dev.lions.unionflow.server.api.dto.finance_workflow.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO de requête pour rejeter une transaction
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-13
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RejectTransactionRequest {
@NotBlank(message = "La raison du rejet est requise")
@Size(min = 10, max = 1000, message = "La raison doit contenir entre 10 et 1000 caractères")
private String reason;
}

View File

@@ -0,0 +1,44 @@
package dev.lions.unionflow.server.api.dto.finance_workflow.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO de réponse pour une action d'approbateur
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-13
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ApproverActionResponse {
private UUID id;
@NotNull
private UUID approverId;
@NotBlank
private String approverName;
@NotBlank
private String approverRole;
@NotBlank
private String decision; // PENDING, APPROVED, REJECTED
private String comment;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime decidedAt;
}

View File

@@ -0,0 +1,47 @@
package dev.lions.unionflow.server.api.dto.finance_workflow.response;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO de réponse pour une ligne budgétaire
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-13
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BudgetLineResponse {
private UUID id;
@NotBlank
private String category; // CONTRIBUTIONS, SAVINGS, SOLIDARITY, etc.
@NotBlank
private String name;
private String description;
@NotNull
private BigDecimal amountPlanned;
@NotNull
private BigDecimal amountRealized;
private String notes;
// Champs calculés
private Double realizationRate;
private BigDecimal variance;
private Boolean isOverBudget;
}

View File

@@ -0,0 +1,92 @@
package dev.lions.unionflow.server.api.dto.finance_workflow.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO de réponse pour un budget
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-13
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class BudgetResponse {
private UUID id;
@NotBlank
private String name;
private String description;
@NotNull
private UUID organizationId;
@NotBlank
private String period; // MONTHLY, QUARTERLY, SEMIANNUAL, ANNUAL
@NotNull
private Integer year;
private Integer month;
@NotBlank
private String status; // DRAFT, ACTIVE, CLOSED, CANCELLED
@Builder.Default
private List<BudgetLineResponse> lines = new ArrayList<>();
@NotNull
private BigDecimal totalPlanned;
@NotNull
private BigDecimal totalRealized;
@NotBlank
private String currency;
@NotNull
private UUID createdById;
@NotNull
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime approvedAt;
private UUID approvedById;
@NotNull
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate startDate;
@NotNull
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate endDate;
private String metadata;
// Champs calculés
private Double realizationRate;
private BigDecimal variance;
private Double varianceRate;
private Boolean isOverBudget;
private Boolean isActive;
private Boolean isCurrentPeriod;
}

View File

@@ -0,0 +1,81 @@
package dev.lions.unionflow.server.api.dto.finance_workflow.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* DTO de réponse pour une approbation de transaction
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-13
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TransactionApprovalResponse {
private UUID id;
@NotNull
private UUID transactionId;
@NotBlank
private String transactionType; // CONTRIBUTION, DEPOSIT, WITHDRAWAL, etc.
@NotNull
private BigDecimal amount;
@NotBlank
private String currency;
@NotNull
private UUID requesterId;
@NotBlank
private String requesterName;
private UUID organizationId;
@NotBlank
private String requiredLevel; // NONE, LEVEL1, LEVEL2, LEVEL3
@NotBlank
private String status; // PENDING, APPROVED, VALIDATED, REJECTED, EXPIRED, CANCELLED
@Builder.Default
private List<ApproverActionResponse> approvers = new ArrayList<>();
private String rejectionReason;
@NotNull
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime expiresAt;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime completedAt;
private String metadata;
// Champs calculés
private Integer approvalCount;
private Integer requiredApprovals;
private Boolean hasAllApprovals;
private Boolean isExpired;
private Boolean isPending;
private Boolean isCompleted;
}

View File

@@ -0,0 +1,51 @@
package dev.lions.unionflow.server.api.dto.formuleabonnement.request;
import dev.lions.unionflow.server.api.enums.formuleabonnement.StatutFormule;
import dev.lions.unionflow.server.api.enums.formuleabonnement.TypeFormule;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
/**
* Requête de création d'une formule d'abonnement.
*/
@Builder
public record CreateFormuleAbonnementRequest(
@NotBlank(message = "Le nom de la formule est obligatoire") @Size(min = 2, max = 100, message = "Le nom de la formule doit contenir entre 2 et 100 caractères") String nom,
@NotBlank(message = "Le code de la formule est obligatoire") @Pattern(regexp = "^[A-Z_]{2,20}$", message = "Le code doit contenir uniquement des lettres majuscules et underscores") String code,
@Size(max = 1000, message = "La description ne peut pas dépasser 1000 caractères") String description,
@NotNull(message = "Le type de formule est obligatoire") TypeFormule type,
@NotNull(message = "Le statut est obligatoire") StatutFormule statut,
@NotNull(message = "Le prix mensuel est obligatoire") @DecimalMin(value = "0.0", inclusive = false) @Digits(integer = 10, fraction = 2) BigDecimal prixMensuel,
@DecimalMin(value = "0.0", inclusive = false) @Digits(integer = 10, fraction = 2) BigDecimal prixAnnuel,
@NotBlank(message = "La devise est obligatoire") @Pattern(regexp = "^[A-Z]{3}$") String devise,
@NotNull(message = "Le nombre maximum de membres est obligatoire") Integer maxMembres,
@NotNull(message = "Le nombre maximum d'administrateurs est obligatoire") Integer maxAdministrateurs,
@NotNull(message = "L'espace de stockage est obligatoire") @DecimalMin(value = "0.1", message = "L'espace de stockage doit être d'au moins 0.1 GB") BigDecimal espaceStockageGB,
@NotNull(message = "Le support technique doit être spécifié") Boolean supportTechnique,
@Pattern(regexp = "^(EMAIL|CHAT|TELEPHONE|PREMIUM)$", message = "Le niveau de support doit être EMAIL, CHAT, TELEPHONE ou PREMIUM") String niveauSupport,
Boolean fonctionnalitesAvancees,
Boolean apiAccess,
Boolean rapportsPersonnalises,
Boolean integrationsTierces,
Boolean sauvegardeAutomatique,
Boolean multiLangues,
Boolean personnalisationInterface,
Boolean formationIncluse,
Integer heuresFormation,
Boolean populaire,
Boolean recommandee,
Integer periodeEssaiJours,
LocalDate dateDebutValidite,
LocalDate dateFinValidite,
Integer ordreAffichage,
@Pattern(regexp = "^#[0-9A-Fa-f]{6}$", message = "La couleur doit être un code hexadécimal valide") String couleur,
@Size(max = 50, message = "Le nom de l'icône ne peut pas dépasser 50 caractères") String icone,
@Size(max = 500, message = "Les notes ne peuvent pas dépasser 500 caractères") String notes) {
}

View File

@@ -0,0 +1,49 @@
package dev.lions.unionflow.server.api.dto.formuleabonnement.request;
import dev.lions.unionflow.server.api.enums.formuleabonnement.StatutFormule;
import dev.lions.unionflow.server.api.enums.formuleabonnement.TypeFormule;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
/**
* Requête de mise à jour d'une formule d'abonnement.
*/
@Builder
public record UpdateFormuleAbonnementRequest(
@Size(min = 2, max = 100, message = "Le nom de la formule doit contenir entre 2 et 100 caractères") String nom,
@Pattern(regexp = "^[A-Z_]{2,20}$", message = "Le code doit contenir uniquement des lettres majuscules et underscores") String code,
@Size(max = 1000, message = "La description ne peut pas dépasser 1000 caractères") String description,
TypeFormule type,
StatutFormule statut,
@DecimalMin(value = "0.0", inclusive = false, message = "Le prix mensuel doit être positif") @Digits(integer = 10, fraction = 2) BigDecimal prixMensuel,
@DecimalMin(value = "0.0", inclusive = false, message = "Le prix annuel doit être positif") @Digits(integer = 10, fraction = 2) BigDecimal prixAnnuel,
@Pattern(regexp = "^[A-Z]{3}$") String devise,
Integer maxMembres,
Integer maxAdministrateurs,
@DecimalMin(value = "0.1", message = "L'espace de stockage doit être d'au moins 0.1 GB") BigDecimal espaceStockageGB,
Boolean supportTechnique,
@Pattern(regexp = "^(EMAIL|CHAT|TELEPHONE|PREMIUM)$", message = "Le niveau de support doit être EMAIL, CHAT, TELEPHONE ou PREMIUM") String niveauSupport,
Boolean fonctionnalitesAvancees,
Boolean apiAccess,
Boolean rapportsPersonnalises,
Boolean integrationsTierces,
Boolean sauvegardeAutomatique,
Boolean multiLangues,
Boolean personnalisationInterface,
Boolean formationIncluse,
Integer heuresFormation,
Boolean populaire,
Boolean recommandee,
Integer periodeEssaiJours,
LocalDate dateDebutValidite,
LocalDate dateFinValidite,
Integer ordreAffichage,
@Pattern(regexp = "^#[0-9A-Fa-f]{6}$", message = "La couleur doit être un code hexadécimal valide") String couleur,
@Size(max = 50, message = "Le nom de l'icône ne peut pas dépasser 50 caractères") String icone,
@Size(max = 500, message = "Les notes ne peuvent pas dépasser 500 caractères") String notes) {
}

View File

@@ -0,0 +1,161 @@
package dev.lions.unionflow.server.api.dto.formuleabonnement.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import dev.lions.unionflow.server.api.enums.formuleabonnement.StatutFormule;
import dev.lions.unionflow.server.api.enums.formuleabonnement.TypeFormule;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse détaillée pour une formule d'abonnement.
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FormuleAbonnementResponse extends BaseResponse {
private String nom;
private String code;
private String description;
private TypeFormule type;
private StatutFormule statut;
private BigDecimal prixMensuel;
private BigDecimal prixAnnuel;
@Builder.Default
private String devise = "XOF";
private Integer maxMembres;
private Integer maxAdministrateurs;
private BigDecimal espaceStockageGB;
private Boolean supportTechnique;
private String niveauSupport;
private Boolean fonctionnalitesAvancees;
private Boolean apiAccess;
private Boolean rapportsPersonnalises;
private Boolean integrationsTierces;
private Boolean sauvegardeAutomatique;
private Boolean multiLangues;
private Boolean personnalisationInterface;
private Boolean formationIncluse;
private Integer heuresFormation;
private Boolean populaire;
private Boolean recommandee;
private Integer periodeEssaiJours;
private LocalDate dateDebutValidite;
private LocalDate dateFinValidite;
private Integer ordreAffichage;
private String couleur;
private String icone;
private String notes;
public boolean isActive() {
return StatutFormule.ACTIVE.equals(statut);
}
public boolean isInactive() {
return StatutFormule.INACTIVE.equals(statut);
}
public boolean isArchivee() {
return StatutFormule.ARCHIVEE.equals(statut);
}
public boolean isValide() {
if (!isActive())
return false;
LocalDate aujourd = LocalDate.now();
if (dateDebutValidite != null && aujourd.isBefore(dateDebutValidite))
return false;
if (dateFinValidite != null && aujourd.isAfter(dateFinValidite))
return false;
return true;
}
public BigDecimal getEconomieAnnuelle() {
if (prixMensuel == null || prixAnnuel == null)
return BigDecimal.ZERO;
BigDecimal coutMensuelAnnuel = prixMensuel.multiply(BigDecimal.valueOf(12));
return coutMensuelAnnuel.subtract(prixAnnuel);
}
public int getPourcentageEconomieAnnuelle() {
if (prixMensuel == null || prixAnnuel == null)
return 0;
BigDecimal coutMensuelAnnuel = prixMensuel.multiply(BigDecimal.valueOf(12));
if (coutMensuelAnnuel.compareTo(BigDecimal.ZERO) > 0) {
return getEconomieAnnuelle()
.multiply(BigDecimal.valueOf(100))
.divide(coutMensuelAnnuel, 0, java.math.RoundingMode.HALF_UP)
.intValue();
}
return 0;
}
public boolean hasPeriodeEssai() {
return periodeEssaiJours != null && periodeEssaiJours > 0;
}
public boolean hasFormation() {
return Boolean.TRUE.equals(formationIncluse) && heuresFormation != null && heuresFormation > 0;
}
public boolean isMiseEnAvant() {
return Boolean.TRUE.equals(populaire) || Boolean.TRUE.equals(recommandee);
}
public String getBadge() {
if (Boolean.TRUE.equals(populaire))
return "POPULAIRE";
if (Boolean.TRUE.equals(recommandee))
return "RECOMMANDÉE";
if (hasPeriodeEssai())
return "ESSAI GRATUIT";
return null;
}
public int getScoreFonctionnalites() {
if (supportTechnique == null && sauvegardeAutomatique == null && fonctionnalitesAvancees == null
&& apiAccess == null && rapportsPersonnalises == null && integrationsTierces == null
&& multiLangues == null && personnalisationInterface == null) {
return 0;
}
int score = 0;
int total = 0;
if (Boolean.TRUE.equals(supportTechnique)) score += 10;
total += 10;
if (Boolean.TRUE.equals(sauvegardeAutomatique)) score += 10;
total += 10;
if (Boolean.TRUE.equals(fonctionnalitesAvancees)) score += 15;
total += 15;
if (Boolean.TRUE.equals(apiAccess)) score += 15;
total += 15;
if (Boolean.TRUE.equals(rapportsPersonnalises)) score += 15;
total += 15;
if (Boolean.TRUE.equals(integrationsTierces)) score += 15;
total += 15;
if (Boolean.TRUE.equals(multiLangues)) score += 10;
total += 10;
if (Boolean.TRUE.equals(personnalisationInterface)) score += 10;
total += 10;
return (score * 100) / total;
}
public String getCssClass() {
if (type == null)
return "formule-default";
return switch (type) {
case BASIC -> "formule-basic";
case STANDARD -> "formule-standard";
case PREMIUM -> "formule-premium";
case ENTERPRISE -> "formule-enterprise";
};
}
}

View File

@@ -0,0 +1,29 @@
package dev.lions.unionflow.server.api.dto.gouvernance;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.gouvernance.NiveauEchelon;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EchelonOrganigrammeDTO extends BaseDTO {
// L'Id physique de l'organisation dans le système (une organisation == un
// échelon)
private String organisationId;
// L'organisation mère / chapeau de cet échelon
private String echelonParentId;
private NiveauEchelon niveau;
private String designation;
private String zoneGeographiqueOuDelegation;
}

View File

@@ -0,0 +1,74 @@
package dev.lions.unionflow.server.api.dto.membre;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* DTO représentant le "compte adhérent" d'un membre de mutuelle/association.
*
* <p>Ce compte est une vue agrégée (non persistée en tant que telle) qui regroupe
* toutes les informations financières du membre :
* <ul>
* <li>Numéro de membre (identifiant lisible, ex : MUF-2026-001)</li>
* <li>Solde cotisations (total payé toutes années)</li>
* <li>Solde épargne (somme des comptes d'épargne actifs)</li>
* <li>Solde total disponible = cotisations + épargne</li>
* <li>Encours crédit (prêts en cours, 0 si pas encore implémenté)</li>
* <li>Capacité d'emprunt estimée (3× l'épargne — règle mutuelle classique)</li>
* </ul>
*
* @param numeroMembre Numéro unique du membre sur la plateforme (ex: MUF-2026-001)
* @param nomComplet Nom et prénom du membre
* @param organisationNom Nom de l'organisation principale (ou null si aucune)
* @param dateAdhesion Date d'inscription sur la plateforme
* @param statutCompte Statut actuel du compte (ACTIF, SUSPENDU, etc.)
*
* @param soldeCotisations Total des cotisations payées (toutes années confondues)
* @param soldeEpargne Solde disponible sur l'ensemble des comptes épargne actifs
* @param soldeBloque Montant bloqué (garantie de prêt)
* @param soldeTotalDisponible soldeCotisations + soldeEpargne - soldeBloque
* @param encoursCreditTotal Montant total des prêts en cours (0 si fonctionnalité non encore activée)
* @param capaciteEmprunt Capacité d'emprunt estimée (3 × soldeEpargne selon règle mutuelle standard)
*
* @param nombreCotisationsPayees Nombre de cotisations payées (historique complet)
* @param nombreCotisationsTotal Nombre total de cotisations (payées + en attente + retard)
* @param nombreCotisationsEnRetard Nombre de cotisations en retard
* @param tauxEngagement Taux de paiement global en % (0-100)
*
* @param nombreComptesEpargne Nombre de comptes épargne actifs
* @param dateCalcul Date/heure du calcul (pour information client)
*
* @author UnionFlow Team
* @version 1.0
*/
public record CompteAdherentResponse(
// ── Identité ──────────────────────────────────────────────────────────
String numeroMembre,
String nomComplet,
String organisationNom,
LocalDate dateAdhesion,
String statutCompte,
// ── Soldes ────────────────────────────────────────────────────────────
BigDecimal soldeCotisations,
BigDecimal soldeEpargne,
BigDecimal soldeBloque,
BigDecimal soldeTotalDisponible,
BigDecimal encoursCreditTotal,
BigDecimal capaciteEmprunt,
// ── Cotisations ───────────────────────────────────────────────────────
Integer nombreCotisationsPayees,
Integer nombreCotisationsTotal,
Integer nombreCotisationsEnRetard,
Integer tauxEngagement,
// ── Épargne ───────────────────────────────────────────────────────────
Integer nombreComptesEpargne,
// ── Méta ──────────────────────────────────────────────────────────────
LocalDate dateCalcul
) implements Serializable {}

View File

@@ -0,0 +1,226 @@
package dev.lions.unionflow.server.api.dto.membre;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
/**
* DTO pour les critères de recherche avancée des membres Permet de filtrer les membres selon de
* multiples critères
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Critères de recherche avancée pour les membres")
public class MembreSearchCriteria {
/** Terme de recherche général (nom, prénom, email) */
@Schema(description = "Terme de recherche général dans nom, prénom ou email", example = "marie")
@Size(max = 100, message = "Le terme de recherche ne peut pas dépasser 100 caractères")
private String query;
/** Recherche par nom exact ou partiel */
@Schema(description = "Filtre par nom (recherche partielle)", example = "Dupont")
@Size(max = 50, message = "Le nom ne peut pas dépasser 50 caractères")
private String nom;
/** Recherche par prénom exact ou partiel */
@Schema(description = "Filtre par prénom (recherche partielle)", example = "Marie")
@Size(max = 50, message = "Le prénom ne peut pas dépasser 50 caractères")
private String prenom;
/** Recherche par email exact ou partiel */
@Schema(description = "Filtre par email (recherche partielle)", example = "@unionflow.com")
@Size(max = 100, message = "L'email ne peut pas dépasser 100 caractères")
private String email;
/** Filtre par numéro de téléphone */
@Schema(description = "Filtre par numéro de téléphone", example = "+221")
@Size(max = 20, message = "Le téléphone ne peut pas dépasser 20 caractères")
private String telephone;
/** Liste des IDs d'organisations */
@Schema(description = "Liste des IDs d'organisations à inclure")
private List<UUID> organisationIds;
/** Liste des rôles à rechercher */
@Schema(description = "Liste des rôles à rechercher", example = "[\"PRESIDENT\", \"SECRETAIRE\"]")
private List<String> roles;
/** Filtre par statut d'activité */
@Schema(description = "Filtre par statut d'activité", example = "ACTIF")
@Pattern(regexp = "^(ACTIF|INACTIF|SUSPENDU|RADIE)$", message = "Statut invalide")
private String statut;
/** Date d'adhésion minimum */
@Schema(description = "Date d'adhésion minimum", example = "2020-01-01")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate dateAdhesionMin;
/** Date d'adhésion maximum */
@Schema(description = "Date d'adhésion maximum", example = "2025-12-31")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate dateAdhesionMax;
/** Âge minimum */
@Schema(description = "Âge minimum", example = "18")
@Min(value = 0, message = "L'âge minimum doit être positif")
@Max(value = 120, message = "L'âge minimum ne peut pas dépasser 120 ans")
private Integer ageMin;
/** Âge maximum */
@Schema(description = "Âge maximum", example = "65")
@Min(value = 0, message = "L'âge maximum doit être positif")
@Max(value = 120, message = "L'âge maximum ne peut pas dépasser 120 ans")
private Integer ageMax;
/** Filtre par région */
@Schema(description = "Filtre par région", example = "Dakar")
@Size(max = 50, message = "La région ne peut pas dépasser 50 caractères")
private String region;
/** Filtre par ville */
@Schema(description = "Filtre par ville", example = "Dakar")
@Size(max = 50, message = "La ville ne peut pas dépasser 50 caractères")
private String ville;
/** Filtre par profession */
@Schema(description = "Filtre par profession", example = "Ingénieur")
@Size(max = 100, message = "La profession ne peut pas dépasser 100 caractères")
private String profession;
/** Filtre par nationalité */
@Schema(description = "Filtre par nationalité", example = "Sénégalaise")
@Size(max = 50, message = "La nationalité ne peut pas dépasser 50 caractères")
private String nationalite;
/** Filtre membres du bureau uniquement */
@Schema(description = "Filtre pour les membres du bureau uniquement")
private Boolean membreBureau;
/** Filtre responsables uniquement */
@Schema(description = "Filtre pour les responsables uniquement")
private Boolean responsable;
/** Inclure les membres inactifs dans la recherche */
@Schema(description = "Inclure les membres inactifs", defaultValue = "false")
@Builder.Default
private Boolean includeInactifs = false;
/**
* Vérifie si au moins un critère de recherche est défini
*
* @return true si au moins un critère est défini
*/
public boolean hasAnyCriteria() {
return query != null && !query.trim().isEmpty()
|| nom != null && !nom.trim().isEmpty()
|| prenom != null && !prenom.trim().isEmpty()
|| email != null && !email.trim().isEmpty()
|| telephone != null && !telephone.trim().isEmpty()
|| organisationIds != null && !organisationIds.isEmpty()
|| roles != null && !roles.isEmpty()
|| statut != null && !statut.trim().isEmpty()
|| dateAdhesionMin != null
|| dateAdhesionMax != null
|| ageMin != null
|| ageMax != null
|| region != null && !region.trim().isEmpty()
|| ville != null && !ville.trim().isEmpty()
|| profession != null && !profession.trim().isEmpty()
|| nationalite != null && !nationalite.trim().isEmpty()
|| membreBureau != null
|| responsable != null;
}
/**
* Valide la cohérence des critères de recherche
*
* @return true si les critères sont cohérents
*/
public boolean isValid() {
// Validation des dates
if (dateAdhesionMin != null && dateAdhesionMax != null) {
if (dateAdhesionMin.isAfter(dateAdhesionMax)) {
return false;
}
}
// Validation des âges
if (ageMin != null && ageMax != null) {
if (ageMin > ageMax) {
return false;
}
}
return true;
}
/** Nettoie les chaînes de caractères (trim et null si vide) */
public void sanitize() {
query = sanitizeString(query);
nom = sanitizeString(nom);
prenom = sanitizeString(prenom);
email = sanitizeString(email);
telephone = sanitizeString(telephone);
statut = sanitizeString(statut);
region = sanitizeString(region);
ville = sanitizeString(ville);
profession = sanitizeString(profession);
nationalite = sanitizeString(nationalite);
}
private String sanitizeString(String str) {
if (str == null) return null;
str = str.trim();
return str.isEmpty() ? null : str;
}
/**
* Retourne une description textuelle des critères actifs
*
* @return Description des critères
*/
public String getDescription() {
StringBuilder sb = new StringBuilder();
if (query != null) sb.append("Recherche: '").append(query).append("' ");
if (nom != null) sb.append("Nom: '").append(nom).append("' ");
if (prenom != null) sb.append("Prénom: '").append(prenom).append("' ");
if (email != null) sb.append("Email: '").append(email).append("' ");
if (statut != null) sb.append("Statut: ").append(statut).append(" ");
if (organisationIds != null && !organisationIds.isEmpty()) {
sb.append("Organisations: ").append(organisationIds.size()).append(" ");
}
if (roles != null && !roles.isEmpty()) {
sb.append("Rôles: ").append(String.join(", ", roles)).append(" ");
}
if (dateAdhesionMin != null) sb.append("Adhésion >= ").append(dateAdhesionMin).append(" ");
if (dateAdhesionMax != null) sb.append("Adhésion <= ").append(dateAdhesionMax).append(" ");
if (ageMin != null) sb.append("Âge >= ").append(ageMin).append(" ");
if (ageMax != null) sb.append("Âge <= ").append(ageMax).append(" ");
if (region != null) sb.append("Région: '").append(region).append("' ");
if (ville != null) sb.append("Ville: '").append(ville).append("' ");
if (profession != null) sb.append("Profession: '").append(profession).append("' ");
if (nationalite != null) sb.append("Nationalité: '").append(nationalite).append("' ");
if (Boolean.TRUE.equals(membreBureau)) sb.append("Membre bureau ");
if (Boolean.TRUE.equals(responsable)) sb.append("Responsable ");
return sb.toString().trim();
}
}

View File

@@ -0,0 +1,227 @@
package dev.lions.unionflow.server.api.dto.membre;
import com.fasterxml.jackson.annotation.JsonProperty;
import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
/**
* DTO pour les résultats de recherche avancée des membres Contient les
* résultats paginés et les
* métadonnées de recherche
*
* @author UnionFlow Team
* @version 1.0
* @since 2025-01-19
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Résultats de recherche avancée des membres avec pagination")
public class MembreSearchResultDTO {
/** Liste des membres trouvés */
@Schema(description = "Liste des membres correspondant aux critères")
private List<MembreSummaryResponse> membres;
/** Nombre total de résultats (toutes pages confondues) */
@Schema(description = "Nombre total de résultats trouvés", example = "247")
private long totalElements;
/** Nombre total de pages */
@Schema(description = "Nombre total de pages", example = "13")
private int totalPages;
/** Numéro de la page actuelle (0-based) */
@Schema(description = "Numéro de la page actuelle", example = "0")
private int currentPage;
/** Taille de la page */
@Schema(description = "Nombre d'éléments par page", example = "20")
private int pageSize;
/** Nombre d'éléments sur la page actuelle */
@Schema(description = "Nombre d'éléments sur cette page", example = "20")
private int numberOfElements;
/** Indique s'il y a une page suivante */
@Schema(description = "Indique s'il y a une page suivante")
private boolean hasNext;
/** Indique s'il y a une page précédente */
@Schema(description = "Indique s'il y a une page précédente")
private boolean hasPrevious;
/** Indique si c'est la première page */
@Schema(description = "Indique si c'est la première page")
@JsonProperty("isFirst")
private boolean isFirst;
/** Indique si c'est la dernière page */
@Schema(description = "Indique si c'est la dernière page")
@JsonProperty("isLast")
private boolean isLast;
/** Critères de recherche utilisés */
@Schema(description = "Critères de recherche qui ont été appliqués")
private MembreSearchCriteria criteria;
/** Temps d'exécution de la recherche en millisecondes */
@Schema(description = "Temps d'exécution de la recherche en ms", example = "45")
private long executionTimeMs;
/** Statistiques de recherche */
@Schema(description = "Statistiques sur les résultats de recherche")
private SearchStatistics statistics;
/** Statistiques sur les résultats de recherche */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Statistiques sur les résultats de recherche")
public static class SearchStatistics {
/** Répartition par statut */
@Schema(description = "Nombre de membres actifs dans les résultats")
private long membresActifs;
@Schema(description = "Nombre de membres inactifs dans les résultats")
private long membresInactifs;
/** Répartition par âge */
@Schema(description = "Âge moyen des membres trouvés")
private double ageMoyen;
@Schema(description = "Âge minimum des membres trouvés")
private int ageMin;
@Schema(description = "Âge maximum des membres trouvés")
private int ageMax;
/** Répartition par organisation */
@Schema(description = "Nombre d'organisations représentées")
private long nombreOrganisations;
/** Répartition par région */
@Schema(description = "Nombre de régions représentées")
private long nombreRegions;
/** Ancienneté moyenne */
@Schema(description = "Ancienneté moyenne en années")
private double ancienneteMoyenne;
}
/** Calcule et met à jour les indicateurs de pagination */
public void calculatePaginationFlags() {
this.isFirst = currentPage == 0;
this.isLast = currentPage >= totalPages - 1;
this.hasPrevious = currentPage > 0;
this.hasNext = currentPage < totalPages - 1;
this.numberOfElements = membres != null ? membres.size() : 0;
}
/**
* Vérifie si les résultats sont vides
*
* @return true si aucun résultat
*/
public boolean isEmpty() {
return membres == null || membres.isEmpty();
}
/**
* Retourne le numéro de la page suivante (1-based pour affichage)
*
* @return Numéro de page suivante ou -1 si pas de page suivante
*/
public int getNextPageNumber() {
return hasNext ? currentPage + 2 : -1;
}
/**
* Retourne le numéro de la page précédente (1-based pour affichage)
*
* @return Numéro de page précédente ou -1 si pas de page précédente
*/
public int getPreviousPageNumber() {
return hasPrevious ? currentPage : -1;
}
/**
* Retourne une description textuelle des résultats
*
* @return Description des résultats
*/
public String getResultDescription() {
if (isEmpty()) {
return "Aucun membre trouvé";
}
if (totalElements == 1) {
return "1 membre trouvé";
}
if (totalPages == 1) {
return String.format("%d membres trouvés", totalElements);
}
int startElement = currentPage * pageSize + 1;
int endElement = Math.min(startElement + numberOfElements - 1, (int) totalElements);
return String.format(
"Membres %d-%d sur %d (page %d/%d)",
startElement, endElement, totalElements, currentPage + 1, totalPages);
}
/**
* Factory method pour créer un résultat vide
*
* @param criteria Critères de recherche
* @return Résultat vide
*/
public static MembreSearchResultDTO empty(MembreSearchCriteria criteria) {
return empty(criteria, 20, 0);
}
/**
* Factory method pour créer un résultat vide avec pageSize spécifique
*
* @param criteria Critères de recherche
* @param pageSize Taille de la page
* @param currentPage Page actuelle
* @return Résultat vide
*/
public static MembreSearchResultDTO empty(MembreSearchCriteria criteria, int pageSize, int currentPage) {
MembreSearchResultDTO result = new MembreSearchResultDTO();
result.setMembres(List.of());
result.setTotalElements(0L);
result.setTotalPages(0);
result.setCurrentPage(currentPage);
result.setPageSize(pageSize);
result.setNumberOfElements(0);
result.setHasNext(false);
result.setHasPrevious(false);
result.isFirst = true; // Assignation directe pour éviter les problèmes avec les setters Lombok
result.isLast = true; // Assignation directe pour éviter les problèmes avec les setters Lombok
result.setCriteria(criteria);
result.setExecutionTimeMs(0L);
// Initialiser statistics avec des valeurs vides
result.setStatistics(SearchStatistics.builder()
.membresActifs(0)
.membresInactifs(0)
.ageMoyen(0.0)
.ageMin(0)
.ageMax(0)
.nombreOrganisations(0)
.nombreRegions(0)
.ancienneteMoyenne(0.0)
.build());
return result;
}
}

View File

@@ -0,0 +1,49 @@
package dev.lions.unionflow.server.api.dto.membre.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
import lombok.Builder;
/**
* Requête de création d'un membre.
*
* <p>
* Immutable via {@code record}. Exclut l'ID,
* les champs d'audit, et le numéroMembre
* (généré côté serveur).
*
* @author UnionFlow Team
* @version 3.0
* @since 2026-02-21
* @param prenom prénom
* @param nom nom de famille
* @param email email
* @param telephone téléphone
* @param telephoneWave numéro Wave
* @param dateNaissance date de naissance
* @param profession profession
* @param photoUrl URL de la photo
* @param statutMatrimonial statut matrimonial
* @param nationalite nationalité
* @param typeIdentite type pièce d'identité
* @param numeroIdentite numéro d'identité
*/
@Builder
public record CreateMembreRequest(
@NotBlank @Size(max = 100) String prenom,
@NotBlank @Size(max = 100) String nom,
@NotBlank @Email @Size(max = 255) String email,
@Size(max = 20) String telephone,
@Size(max = 13) String telephoneWave,
@NotNull LocalDate dateNaissance,
@Size(max = 100) String profession,
@Size(max = 500) String photoUrl,
@Size(max = 50) String statutMatrimonial,
@Size(max = 100) String nationalite,
@Size(max = 50) String typeIdentite,
@Size(max = 100) String numeroIdentite) {
}

View File

@@ -0,0 +1,26 @@
package dev.lions.unionflow.server.api.dto.membre.request;
import jakarta.validation.constraints.*;
import java.time.LocalDate;
import lombok.Builder;
/**
* Requête pour mettre à jour un membre existant.
*/
@Builder
public record UpdateMembreRequest(
@NotBlank @Size(max = 100) String prenom,
@NotBlank @Size(max = 100) String nom,
@NotBlank @Email @Size(max = 255) String email,
@Size(max = 20) String telephone,
@Size(max = 13) String telephoneWave,
@NotNull LocalDate dateNaissance,
@Size(max = 100) String profession,
@Size(max = 500) String photoUrl,
@Size(max = 50) String statutMatrimonial,
@Size(max = 100) String nationalite,
@Size(max = 50) String typeIdentite,
@Size(max = 100) String numeroIdentite,
Boolean actif) {
}

View File

@@ -0,0 +1,70 @@
package dev.lions.unionflow.server.api.dto.membre.response;
import dev.lions.unionflow.server.api.dto.base.BaseResponse;
import java.time.LocalDate;
import java.util.UUID;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* Réponse contenant les données d'un membre.
*
* <p>
* Classe Lombok (getters/setters) pour la
* compatibilité avec les frameworks d'affichage
* (PrimeFaces, etc.).
*
* @author UnionFlow Team
* @version 3.0
* @since 2026-02-21
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MembreResponse extends BaseResponse {
// ── Identité ───────────────────────────────
private String numeroMembre;
private UUID keycloakId;
private String prenom;
private String nom;
private String nomComplet;
private String email;
private String telephone;
private String telephoneWave;
private LocalDate dateNaissance;
private int age;
// ── Personnel ──────────────────────────────
private String profession;
private String photoUrl;
private String statutMatrimonial;
private String statutMatrimonialLibelle;
private String nationalite;
private String typeIdentite;
private String typeIdentiteLibelle;
private String numeroIdentite;
// ── KYC / LCB-FT ───────────────────────────
private String niveauVigilanceKyc;
private String statutKyc;
private LocalDate dateVerificationIdentite;
// ── Statut ─────────────────────────────────
private String statutCompte;
private String statutCompteLibelle;
private String statutCompteSeverity;
private List<String> roles;
// ── Adhésion (contexte organisation) ───────
private UUID organisationId;
private String associationNom;
private LocalDate dateAdhesion;
}

View File

@@ -0,0 +1,24 @@
package dev.lions.unionflow.server.api.dto.membre.response;
import java.util.UUID;
import java.util.List;
/**
* DTO de réponse résumé pour Membre (listes et optimisations).
*/
public record MembreSummaryResponse(
UUID id,
String numeroMembre,
String prenom,
String nom,
String email,
String telephone,
String profession,
String statutCompte,
String statutCompteLibelle,
String statutCompteSeverity,
Boolean actif,
List<String> roles,
UUID organisationId,
String associationNom) {
}

View File

@@ -0,0 +1,49 @@
package dev.lions.unionflow.server.api.dto.mutuelle.credit;
import dev.lions.unionflow.server.api.enums.mutuelle.credit.TypeCredit;
import dev.lions.unionflow.server.api.enums.mutuelle.credit.TypeGarantie;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.math.BigDecimal;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Requête initiale pour une demande de financement (Crédit)")
public class DemandeCreditRequest {
@NotBlank(message = "L'ID du demandeur est obligatoire")
private String membreId;
@NotNull(message = "Le type de crédit est obligatoire")
private TypeCredit typeCredit;
@NotNull(message = "Le montant demandé est obligatoire")
@DecimalMin(value = "1.0", message = "Le montant demandé doit être positif")
private BigDecimal montantDemande;
@Min(value = 1, message = "La durée (en mois) doit être au moins de 1 mois")
private Integer dureeMois;
@Schema(description = "Compte épargne adossé (si remboursement automatique ou nantissement)")
private String compteLieId;
@NotBlank(message = "Le motif détaillé du financement est requis (Plan d'affaires, usage...)")
private String justificationDetaillee;
@Schema(description = "Liste des IDs de documents justificatifs attachés au dossier")
private List<String> documentIds;
@Schema(description = "Liste des garanties proposées avec la demande")
private List<GarantieDemandeDTO> garantiesProposees;
}

View File

@@ -0,0 +1,48 @@
package dev.lions.unionflow.server.api.dto.mutuelle.credit;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.mutuelle.credit.StatutDemandeCredit;
import dev.lions.unionflow.server.api.enums.mutuelle.credit.TypeCredit;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Réponse avec le tableau de bord résumé d'un dossier de crédit")
public class DemandeCreditResponse extends BaseDTO {
private String numeroDossier;
private String membreId;
private TypeCredit typeCredit;
private String compteLieId;
private BigDecimal montantDemande;
private Integer dureeMoisDemande;
// Conditions actées par le comité de crédit (peuvent différer de la demande)
private BigDecimal montantApprouve;
private Integer dureeMoisApprouvee;
private BigDecimal tauxInteretAnnuel; // Pourcentage
private BigDecimal coutTotalCredit; // Total Intérêts
private StatutDemandeCredit statut;
private String notesComite;
private LocalDate dateSoumission;
private LocalDate dateValidation;
private LocalDate datePremierEcheance;
@Schema(description = "Aperçu des échéances générées pour le tableau d'amortissement")
private List<EcheanceCreditDTO> echeancier;
}

View File

@@ -0,0 +1,44 @@
package dev.lions.unionflow.server.api.dto.mutuelle.credit;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.mutuelle.credit.StatutEcheanceCredit;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.LocalDate;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "Représente une ligne unique du tableau d'amortissement d'un crédit")
public class EcheanceCreditDTO extends BaseDTO {
private String demandeCreditId;
@Schema(description = "Indice de l'échéance (ex: mois 1, 2, 3...)")
private Integer ordre;
private LocalDate dateEcheancePrevue;
private LocalDate datePaiementEffectif;
private BigDecimal capitalAmorti;
private BigDecimal interetsDeLaPeriode;
private BigDecimal montantTotalExigible;
private BigDecimal capitalRestantDu;
// Pénalités éventuelles additionnelles liées au retard
private BigDecimal penalitesRetard;
// Somme physiquement encaissée pour cette traite à l'instant T
private BigDecimal montantRegle;
private StatutEcheanceCredit statut;
}

Some files were not shown because too many files have changed in this diff Show More