commit 4a0c5f9d3307ce65e350a3056eb03a3e8ba50cab Author: dahoud Date: Wed Dec 10 01:08:17 2025 +0000 Configure Maven repository for unionflow-server-api dependency diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..ccff9c8 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,91 @@ +#### +# Dockerfile de production pour UnionFlow Server (Backend) +# Multi-stage build optimisé avec sécurité renforcée +#### + +## Stage 1 : Build avec Maven +FROM maven:3.9.6-eclipse-temurin-17 AS builder + +WORKDIR /app + +# Copier les fichiers de configuration Maven +COPY pom.xml . +COPY ../unionflow-server-api/pom.xml ../unionflow-server-api/ + +# Télécharger les dépendances (cache Docker) +RUN mvn dependency:go-offline -B -pl unionflow-server-impl-quarkus -am + +# Copier le code source +COPY src ./src + +# Construire l'application avec profil production +RUN mvn clean package -DskipTests -B -Dquarkus.profile=prod -pl unionflow-server-impl-quarkus + +## Stage 2 : Image de production optimisée +FROM registry.access.redhat.com/ubi8/openjdk-17:1.18 + +ENV LANGUAGE='en_US:en' + +# Configuration des variables d'environnement pour production +ENV QUARKUS_PROFILE=prod +ENV QUARKUS_HTTP_PORT=8085 +ENV QUARKUS_HTTP_HOST=0.0.0.0 + +# Configuration Base de données (à surcharger via variables d'environnement) +ENV DB_URL=jdbc:postgresql://postgresql:5432/unionflow +ENV DB_USERNAME=unionflow +ENV DB_PASSWORD=changeme + +# Configuration Keycloak/OIDC (production) +ENV QUARKUS_OIDC_AUTH_SERVER_URL=https://security.lions.dev/realms/unionflow +ENV QUARKUS_OIDC_CLIENT_ID=unionflow-server +ENV KEYCLOAK_CLIENT_SECRET=changeme +ENV QUARKUS_OIDC_TLS_VERIFICATION=required + +# Configuration CORS pour production +ENV CORS_ORIGINS=https://unionflow.lions.dev,https://security.lions.dev +ENV QUARKUS_HTTP_CORS_ORIGINS=${CORS_ORIGINS} + +# Configuration Wave Money (optionnel) +ENV WAVE_API_KEY= +ENV WAVE_API_SECRET= +ENV WAVE_API_BASE_URL=https://api.wave.com/v1 +ENV WAVE_ENVIRONMENT=production +ENV WAVE_WEBHOOK_SECRET= + +# Installer curl pour les health checks +USER root +RUN microdnf install curl -y && microdnf clean all +RUN mkdir -p /app/logs && chown -R 185:185 /app/logs +USER 185 + +# Copier l'application depuis le builder +COPY --from=builder --chown=185 /app/target/quarkus-app/lib/ /deployments/lib/ +COPY --from=builder --chown=185 /app/target/quarkus-app/*.jar /deployments/ +COPY --from=builder --chown=185 /app/target/quarkus-app/app/ /deployments/app/ +COPY --from=builder --chown=185 /app/target/quarkus-app/quarkus/ /deployments/quarkus/ + +# Exposer le port +EXPOSE 8085 + +# Variables JVM optimisées pour production avec sécurité +ENV JAVA_OPTS="-Xmx1g -Xms512m \ + -XX:+UseG1GC \ + -XX:MaxGCPauseMillis=200 \ + -XX:+UseStringDeduplication \ + -XX:+ParallelRefProcEnabled \ + -XX:+HeapDumpOnOutOfMemoryError \ + -XX:HeapDumpPath=/app/logs/heapdump.hprof \ + -Djava.security.egd=file:/dev/./urandom \ + -Djava.awt.headless=true \ + -Dfile.encoding=UTF-8 \ + -Djava.util.logging.manager=org.jboss.logmanager.LogManager \ + -Dquarkus.profile=${QUARKUS_PROFILE}" + +# Point d'entrée avec profil production +ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /deployments/quarkus-run.jar"] + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8085/q/health/ready || exit 1 + diff --git a/[Help b/[Help new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..5b3c185 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,43 @@ +version: '3.8' + +services: + postgres-dev: + image: postgres:15-alpine + container_name: unionflow-postgres-dev + environment: + POSTGRES_DB: unionflow_dev + POSTGRES_USER: unionflow_dev + POSTGRES_PASSWORD: dev123 + POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" + ports: + - "5432:5432" + volumes: + - postgres_dev_data:/var/lib/postgresql/data + - ./src/main/resources/db/init:/docker-entrypoint-initdb.d + networks: + - unionflow-dev + healthcheck: + test: ["CMD-SHELL", "pg_isready -U unionflow_dev -d unionflow_dev"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + adminer: + image: adminer:4.8.1 + container_name: unionflow-adminer + ports: + - "8081:8080" + networks: + - unionflow-dev + depends_on: + - postgres-dev + restart: unless-stopped + +volumes: + postgres_dev_data: + driver: local + +networks: + unionflow-dev: + driver: bridge diff --git a/mvn b/mvn new file mode 100644 index 0000000..e69de29 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b044d05 --- /dev/null +++ b/pom.xml @@ -0,0 +1,337 @@ + + + 4.0.0 + + dev.lions.unionflow + unionflow-server-impl-quarkus + 1.0.0 + jar + + UnionFlow Server Implementation (Quarkus) + Implémentation Quarkus du serveur UnionFlow + + + 17 + 17 + UTF-8 + + 3.15.1 + io.quarkus.platform + quarkus-bom + + + 0.8.11 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + + dev.lions.unionflow + unionflow-server-api + 1.0.0 + + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-jackson + + + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-jdbc-h2 + + + io.quarkus + quarkus-flyway + + + + + io.quarkus + quarkus-oidc + + + io.quarkus + quarkus-keycloak-authorization + + + + + io.quarkus + quarkus-config-yaml + + + io.quarkus + quarkus-smallrye-health + + + + + io.quarkus + quarkus-smallrye-openapi + + + + + io.quarkus + quarkus-hibernate-validator + + + + + org.projectlombok + lombok + 1.18.30 + provided + + + + + org.apache.poi + poi + 5.2.5 + + + org.apache.poi + poi-ooxml + 5.2.5 + + + org.apache.poi + poi-ooxml-lite + 5.2.5 + + + org.apache.poi + poi-scratchpad + 5.2.5 + + + + + org.apache.commons + commons-csv + 1.10.0 + + + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-junit5-mockito + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-test-security + test + + + org.assertj + assertj-core + 3.24.2 + test + + + org.mockito + mockito-core + 5.7.0 + test + + + + + + gitea + Gitea Maven Repository + https://git.lions.dev/api/packages/lionsdev/maven + + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + UTF-8 + + + org.projectlombok + lombok + 1.18.30 + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + false + false + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + + prepare-agent + + + + report + test + + report + + + + + **/*$*Builder*.class + **/Membre$MembreBuilder.class + + + + + check + + check + + + + + **/*$*Builder*.class + **/Membre$MembreBuilder.class + + + + BUNDLE + + + LINE + COVEREDRATIO + 1.00 + + + BRANCH + COVEREDRATIO + 1.00 + + + INSTRUCTION + COVEREDRATIO + 1.00 + + + METHOD + COVEREDRATIO + 1.00 + + + + + + + + + + + + + + native + + + native + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + + + + native-image + + + + + + + + + \ No newline at end of file diff --git a/setup-postgres.sql b/setup-postgres.sql new file mode 100644 index 0000000..44f7f2a --- /dev/null +++ b/setup-postgres.sql @@ -0,0 +1,27 @@ +-- Script de configuration PostgreSQL pour UnionFlow +-- Exécuter en tant que superuser (postgres) + +-- Créer l'utilisateur unionflow s'il n'existe pas +DO $$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_catalog.pg_user WHERE usename = 'unionflow') THEN + CREATE USER unionflow WITH PASSWORD 'unionflow123'; + END IF; +END +$$; + +-- Créer la base de données unionflow si elle n'existe pas +SELECT 'CREATE DATABASE unionflow OWNER unionflow' +WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'unionflow')\gexec + +-- Donner tous les privilèges à l'utilisateur unionflow +GRANT ALL PRIVILEGES ON DATABASE unionflow TO unionflow; + +-- Se connecter à la base unionflow et donner les privilèges sur le schéma public +\c unionflow +GRANT ALL ON SCHEMA public TO unionflow; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO unionflow; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO unionflow; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO unionflow; +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO unionflow; + diff --git a/src/main/java/de/lions/unionflow/server/auth/AuthCallbackResource.java b/src/main/java/de/lions/unionflow/server/auth/AuthCallbackResource.java new file mode 100644 index 0000000..e77af23 --- /dev/null +++ b/src/main/java/de/lions/unionflow/server/auth/AuthCallbackResource.java @@ -0,0 +1,136 @@ +package de.lions.unionflow.server.auth; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; +import org.jboss.logging.Logger; + +/** + * Resource temporaire pour gérer les callbacks d'authentification OAuth2/OIDC depuis l'application + * mobile. + */ +@Path("/auth") +public class AuthCallbackResource { + + private static final Logger log = Logger.getLogger(AuthCallbackResource.class); + + /** + * Endpoint de callback pour l'authentification OAuth2/OIDC. Redirige vers l'application mobile + * avec les paramètres reçus. + */ + @GET + @Path("/callback") + public Response handleCallback( + @QueryParam("code") String code, + @QueryParam("state") String state, + @QueryParam("session_state") String sessionState, + @QueryParam("error") String error, + @QueryParam("error_description") String errorDescription) { + + try { + // Log des paramètres reçus pour debug + log.infof("=== CALLBACK DEBUG === Code: %s, State: %s, Session State: %s, Error: %s, Error Description: %s", + code, state, sessionState, error, errorDescription); + + // URL de redirection simple vers l'application mobile + String redirectUrl = "dev.lions.unionflow-mobile://callback"; + + // Si nous avons un code d'autorisation, c'est un succès + if (code != null && !code.isEmpty()) { + redirectUrl += "?code=" + code; + if (state != null && !state.isEmpty()) { + redirectUrl += "&state=" + state; + } + } else if (error != null) { + redirectUrl += "?error=" + error; + if (errorDescription != null) { + redirectUrl += "&error_description=" + errorDescription; + } + } + + // Page HTML simple qui redirige automatiquement vers l'app mobile + String html = + """ + + + + Redirection vers UnionFlow + + + + + +
+

🔐 Authentification réussie

+
+

Redirection vers l'application UnionFlow...

+

Si la redirection ne fonctionne pas automatiquement, + cliquez ici

+
+ + + +""" + .formatted(redirectUrl, redirectUrl, redirectUrl); + + return Response.ok(html).type("text/html").build(); + + } catch (Exception e) { + // En cas d'erreur, retourner une page d'erreur simple + String errorHtml = + """ + + + Erreur d'authentification + +

❌ Erreur d'authentification

+

Une erreur s'est produite lors de la redirection.

+

Veuillez fermer cette page et réessayer.

+ + + """; + return Response.status(500).entity(errorHtml).type("text/html").build(); + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/UnionFlowServerApplication.java b/src/main/java/dev/lions/unionflow/server/UnionFlowServerApplication.java new file mode 100644 index 0000000..45d6000 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/UnionFlowServerApplication.java @@ -0,0 +1,35 @@ +package dev.lions.unionflow.server; + +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.QuarkusApplication; +import io.quarkus.runtime.annotations.QuarkusMain; +import jakarta.enterprise.context.ApplicationScoped; +import org.jboss.logging.Logger; + +/** + * Application principale UnionFlow Server + * + * @author Lions Dev Team + * @version 1.0.0 + */ +@QuarkusMain +@ApplicationScoped +public class UnionFlowServerApplication implements QuarkusApplication { + + private static final Logger LOG = Logger.getLogger(UnionFlowServerApplication.class); + + public static void main(String... args) { + Quarkus.run(UnionFlowServerApplication.class, args); + } + + @Override + public int run(String... args) throws Exception { + LOG.info("🚀 UnionFlow Server démarré avec succès!"); + LOG.info("📊 API disponible sur http://localhost:8080"); + LOG.info("📖 Documentation OpenAPI sur http://localhost:8080/q/swagger-ui"); + LOG.info("💚 Health check sur http://localhost:8080/health"); + + Quarkus.waitForExit(); + return 0; + } +} diff --git a/src/main/java/dev/lions/unionflow/server/dto/EvenementMobileDTO.java b/src/main/java/dev/lions/unionflow/server/dto/EvenementMobileDTO.java new file mode 100644 index 0000000..26b4157 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/dto/EvenementMobileDTO.java @@ -0,0 +1,143 @@ +package dev.lions.unionflow.server.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import dev.lions.unionflow.server.entity.Evenement; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * DTO pour l'API mobile - Mapping des champs de l'entité Evenement vers le format attendu par + * l'application mobile Flutter + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +public class EvenementMobileDTO { + + private UUID id; + private String titre; + private String description; + private LocalDateTime dateDebut; + private LocalDateTime dateFin; + private String lieu; + private String adresse; + private String ville; + private String codePostal; + + // Mapping: typeEvenement -> type + private String type; + + // Mapping: statut -> statut (OK) + private String statut; + + // Mapping: capaciteMax -> maxParticipants + private Integer maxParticipants; + + // Nombre de participants actuels (calculé depuis les inscriptions) + private Integer participantsActuels; + + // IDs et noms pour les relations + private UUID organisateurId; + private String organisateurNom; + private UUID organisationId; + private String organisationNom; + + // Priorité (à ajouter dans l'entité si nécessaire) + private String priorite; + + // Mapping: visiblePublic -> estPublic + private Boolean estPublic; + + // Mapping: inscriptionRequise -> inscriptionRequise (OK) + private Boolean inscriptionRequise; + + // Mapping: prix -> cout + private BigDecimal cout; + + // Devise + private String devise; + + // Tags (à implémenter si nécessaire) + private String[] tags; + + // URLs + private String imageUrl; + private String documentUrl; + + // Notes + private String notes; + + // Dates de création/modification + private LocalDateTime dateCreation; + private LocalDateTime dateModification; + + // Actif + private Boolean actif; + + /** + * Convertit une entité Evenement en DTO mobile + * + * @param evenement L'entité à convertir + * @return Le DTO mobile + */ + public static EvenementMobileDTO fromEntity(Evenement evenement) { + if (evenement == null) { + return null; + } + + return EvenementMobileDTO.builder() + .id(evenement.getId()) // Utilise getId() depuis BaseEntity + .titre(evenement.getTitre()) + .description(evenement.getDescription()) + .dateDebut(evenement.getDateDebut()) + .dateFin(evenement.getDateFin()) + .lieu(evenement.getLieu()) + .adresse(evenement.getAdresse()) + .ville(null) // Pas de champ ville dans l'entité + .codePostal(null) // Pas de champ codePostal dans l'entité + // Mapping des enums + .type(evenement.getTypeEvenement() != null ? evenement.getTypeEvenement().name() : null) + .statut(evenement.getStatut() != null ? evenement.getStatut().name() : "PLANIFIE") + // Mapping des champs renommés + .maxParticipants(evenement.getCapaciteMax()) + .participantsActuels(evenement.getNombreInscrits()) + // Relations (gestion sécurisée des lazy loading) + .organisateurId(evenement.getOrganisateur() != null ? evenement.getOrganisateur().getId() : null) + .organisateurNom(evenement.getOrganisateur() != null ? evenement.getOrganisateur().getNomComplet() : null) + .organisationId(evenement.getOrganisation() != null ? evenement.getOrganisation().getId() : null) + .organisationNom(evenement.getOrganisation() != null ? evenement.getOrganisation().getNom() : null) + // Priorité (valeur par défaut) + .priorite("MOYENNE") + // Mapping booléens + .estPublic(evenement.getVisiblePublic()) + .inscriptionRequise(evenement.getInscriptionRequise()) + // Mapping prix -> cout + .cout(evenement.getPrix()) + .devise("XOF") + // Tags vides pour l'instant + .tags(new String[] {}) + // URLs (à implémenter si nécessaire) + .imageUrl(null) + .documentUrl(null) + // Notes + .notes(evenement.getInstructionsParticulieres()) + // Dates + .dateCreation(evenement.getDateCreation()) + .dateModification(evenement.getDateModification()) + // Actif + .actif(evenement.getActif()) + .build(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/Adhesion.java b/src/main/java/dev/lions/unionflow/server/entity/Adhesion.java new file mode 100644 index 0000000..e5fbd8a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Adhesion.java @@ -0,0 +1,132 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Adhesion avec UUID + * Représente une demande d'adhésion d'un membre à une organisation + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@Entity +@Table( + name = "adhesions", + indexes = { + @Index(name = "idx_adhesion_membre", columnList = "membre_id"), + @Index(name = "idx_adhesion_organisation", columnList = "organisation_id"), + @Index(name = "idx_adhesion_reference", columnList = "numero_reference", unique = true), + @Index(name = "idx_adhesion_statut", columnList = "statut"), + @Index(name = "idx_adhesion_date_demande", columnList = "date_demande") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Adhesion extends BaseEntity { + + @NotBlank + @Column(name = "numero_reference", unique = true, nullable = false, length = 50) + private String numeroReference; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id", nullable = false) + private Membre membre; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id", nullable = false) + private Organisation organisation; + + @NotNull + @Column(name = "date_demande", nullable = false) + private LocalDate dateDemande; + + @NotNull + @DecimalMin(value = "0.0", message = "Le montant des frais d'adhésion doit être positif") + @Digits(integer = 10, fraction = 2) + @Column(name = "frais_adhesion", nullable = false, precision = 12, scale = 2) + private BigDecimal fraisAdhesion; + + @Builder.Default + @DecimalMin(value = "0.0", message = "Le montant payé doit être positif") + @Digits(integer = 10, fraction = 2) + @Column(name = "montant_paye", nullable = false, precision = 12, scale = 2) + private BigDecimal montantPaye = BigDecimal.ZERO; + + @NotBlank + @Pattern(regexp = "^[A-Z]{3}$", message = "Le code devise doit être un code ISO à 3 lettres") + @Column(name = "code_devise", nullable = false, length = 3) + private String codeDevise; + + @NotBlank + @Pattern( + regexp = "^(EN_ATTENTE|APPROUVEE|REJETEE|ANNULEE|EN_PAIEMENT|PAYEE)$", + message = "Statut invalide") + @Column(name = "statut", nullable = false, length = 30) + private String statut; + + @Column(name = "date_approbation") + private LocalDate dateApprobation; + + @Column(name = "date_paiement") + private LocalDateTime datePaiement; + + @Size(max = 20) + @Column(name = "methode_paiement", length = 20) + private String methodePaiement; + + @Size(max = 100) + @Column(name = "reference_paiement", length = 100) + private String referencePaiement; + + @Size(max = 1000) + @Column(name = "motif_rejet", length = 1000) + private String motifRejet; + + @Size(max = 1000) + @Column(name = "observations", length = 1000) + private String observations; + + @Column(name = "approuve_par", length = 255) + private String approuvePar; + + @Column(name = "date_validation") + private LocalDate dateValidation; + + /** Méthode métier pour vérifier si l'adhésion est payée intégralement */ + public boolean isPayeeIntegralement() { + return montantPaye != null + && fraisAdhesion != null + && montantPaye.compareTo(fraisAdhesion) >= 0; + } + + /** Méthode métier pour vérifier si l'adhésion est en attente de paiement */ + public boolean isEnAttentePaiement() { + return "APPROUVEE".equals(statut) && !isPayeeIntegralement(); + } + + /** Méthode métier pour calculer le montant restant à payer */ + 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; + } +} + + + diff --git a/src/main/java/dev/lions/unionflow/server/entity/Adresse.java b/src/main/java/dev/lions/unionflow/server/entity/Adresse.java new file mode 100644 index 0000000..c2aa2d0 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Adresse.java @@ -0,0 +1,154 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.adresse.TypeAdresse; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Adresse pour la gestion des adresses des organisations, membres et événements + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "adresses", + indexes = { + @Index(name = "idx_adresse_ville", columnList = "ville"), + @Index(name = "idx_adresse_pays", columnList = "pays"), + @Index(name = "idx_adresse_type", columnList = "type_adresse"), + @Index(name = "idx_adresse_organisation", columnList = "organisation_id"), + @Index(name = "idx_adresse_membre", columnList = "membre_id"), + @Index(name = "idx_adresse_evenement", columnList = "evenement_id") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Adresse extends BaseEntity { + + /** Type d'adresse */ + @Enumerated(EnumType.STRING) + @Column(name = "type_adresse", nullable = false, length = 50) + private TypeAdresse typeAdresse; + + /** Adresse complète */ + @Column(name = "adresse", length = 500) + private String adresse; + + /** Complément d'adresse */ + @Column(name = "complement_adresse", length = 200) + private String complementAdresse; + + /** Code postal */ + @Column(name = "code_postal", length = 20) + private String codePostal; + + /** Ville */ + @Column(name = "ville", length = 100) + private String ville; + + /** Région */ + @Column(name = "region", length = 100) + private String region; + + /** Pays */ + @Column(name = "pays", length = 100) + private String pays; + + /** Coordonnées géographiques - Latitude */ + @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) + @Column(name = "latitude", precision = 9, scale = 6) + private BigDecimal latitude; + + /** Coordonnées géographiques - Longitude */ + @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) + @Column(name = "longitude", precision = 9, scale = 6) + private BigDecimal longitude; + + /** Adresse principale (une seule par entité) */ + @Builder.Default + @Column(name = "principale", nullable = false) + private Boolean principale = false; + + /** Libellé personnalisé */ + @Column(name = "libelle", length = 100) + private String libelle; + + /** Notes et commentaires */ + @Column(name = "notes", length = 500) + private String notes; + + // Relations + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id") + private Organisation organisation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id") + private Membre membre; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "evenement_id") + private Evenement evenement; + + /** Méthode métier pour obtenir l'adresse complète formatée */ + public String getAdresseComplete() { + StringBuilder sb = new StringBuilder(); + if (adresse != null && !adresse.isEmpty()) { + sb.append(adresse); + } + if (complementAdresse != null && !complementAdresse.isEmpty()) { + if (sb.length() > 0) sb.append(", "); + sb.append(complementAdresse); + } + if (codePostal != null && !codePostal.isEmpty()) { + if (sb.length() > 0) sb.append(", "); + sb.append(codePostal); + } + if (ville != null && !ville.isEmpty()) { + if (sb.length() > 0) sb.append(" "); + sb.append(ville); + } + if (region != null && !region.isEmpty()) { + if (sb.length() > 0) sb.append(", "); + sb.append(region); + } + if (pays != null && !pays.isEmpty()) { + if (sb.length() > 0) sb.append(", "); + sb.append(pays); + } + return sb.toString(); + } + + /** Méthode métier pour vérifier si l'adresse a des coordonnées GPS */ + public boolean hasCoordinates() { + return latitude != null && longitude != null; + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); // Appelle le onCreate de BaseEntity + if (typeAdresse == null) { + typeAdresse = dev.lions.unionflow.server.api.enums.adresse.TypeAdresse.AUTRE; + } + if (principale == null) { + principale = false; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/AuditLog.java b/src/main/java/dev/lions/unionflow/server/entity/AuditLog.java new file mode 100644 index 0000000..6ec2bee --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/AuditLog.java @@ -0,0 +1,81 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; + +/** + * Entité pour les logs d'audit + * Enregistre toutes les actions importantes du système + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@Entity +@Table(name = "audit_logs", indexes = { + @Index(name = "idx_audit_date_heure", columnList = "date_heure"), + @Index(name = "idx_audit_utilisateur", columnList = "utilisateur"), + @Index(name = "idx_audit_module", columnList = "module"), + @Index(name = "idx_audit_type_action", columnList = "type_action"), + @Index(name = "idx_audit_severite", columnList = "severite") +}) +@Getter +@Setter +public class AuditLog extends BaseEntity { + + @Column(name = "type_action", nullable = false, length = 50) + private String typeAction; + + @Column(name = "severite", nullable = false, length = 20) + private String severite; + + @Column(name = "utilisateur", length = 255) + private String utilisateur; + + @Column(name = "role", length = 50) + private String role; + + @Column(name = "module", length = 50) + private String module; + + @Column(name = "description", length = 500) + private String description; + + @Column(name = "details", columnDefinition = "TEXT") + private String details; + + @Column(name = "ip_address", length = 45) + private String ipAddress; + + @Column(name = "user_agent", length = 500) + private String userAgent; + + @Column(name = "session_id", length = 255) + private String sessionId; + + @Column(name = "date_heure", nullable = false) + private LocalDateTime dateHeure; + + @Column(name = "donnees_avant", columnDefinition = "TEXT") + private String donneesAvant; + + @Column(name = "donnees_apres", columnDefinition = "TEXT") + private String donneesApres; + + @Column(name = "entite_id", length = 255) + private String entiteId; + + @Column(name = "entite_type", length = 100) + private String entiteType; + + @PrePersist + protected void onCreate() { + if (dateHeure == null) { + dateHeure = LocalDateTime.now(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/BaseEntity.java b/src/main/java/dev/lions/unionflow/server/entity/BaseEntity.java new file mode 100644 index 0000000..5a1ef42 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/BaseEntity.java @@ -0,0 +1,141 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * Classe de base pour les entités UnionFlow utilisant UUID comme identifiant + * + *

Remplace PanacheEntity pour utiliser UUID au lieu de Long comme ID. + * Fournit les fonctionnalités de base de Panache avec UUID. + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +@MappedSuperclass +public abstract class BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(name = "id", updatable = false, nullable = false) + private UUID id; + + @Column(name = "date_creation", nullable = false, updatable = false) + protected LocalDateTime dateCreation; + + @Column(name = "date_modification") + protected LocalDateTime dateModification; + + @Column(name = "cree_par", length = 255) + protected String creePar; + + @Column(name = "modifie_par", length = 255) + protected String modifiePar; + + @Version + @Column(name = "version") + protected Long version; + + @Column(name = "actif", nullable = false) + protected Boolean actif = true; + + // Constructeur par défaut + public BaseEntity() { + this.dateCreation = LocalDateTime.now(); + this.actif = true; + this.version = 0L; + } + + // Getters et Setters + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public LocalDateTime getDateCreation() { + return dateCreation; + } + + public void setDateCreation(LocalDateTime dateCreation) { + this.dateCreation = dateCreation; + } + + public LocalDateTime getDateModification() { + return dateModification; + } + + public void setDateModification(LocalDateTime dateModification) { + this.dateModification = dateModification; + } + + public String getCreePar() { + return creePar; + } + + public void setCreePar(String creePar) { + this.creePar = creePar; + } + + public String getModifiePar() { + return modifiePar; + } + + public void setModifiePar(String modifiePar) { + this.modifiePar = modifiePar; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public Boolean getActif() { + return actif; + } + + public void setActif(Boolean actif) { + this.actif = actif; + } + + // Callbacks JPA + @PrePersist + protected void onCreate() { + if (this.dateCreation == null) { + this.dateCreation = LocalDateTime.now(); + } + if (this.actif == null) { + this.actif = true; + } + if (this.version == null) { + this.version = 0L; + } + } + + @PreUpdate + protected void onUpdate() { + this.dateModification = LocalDateTime.now(); + } + + // Méthodes utilitaires Panache-like + public void persist() { + // Cette méthode sera implémentée par les repositories ou services + // Pour l'instant, elle est là pour compatibilité avec le code existant + throw new UnsupportedOperationException( + "Utilisez le repository approprié pour persister cette entité"); + } + + public static T findById(UUID id) { + // Cette méthode sera implémentée par les repositories + throw new UnsupportedOperationException( + "Utilisez le repository approprié pour rechercher par ID"); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/CompteComptable.java b/src/main/java/dev/lions/unionflow/server/entity/CompteComptable.java new file mode 100644 index 0000000..6408807 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/CompteComptable.java @@ -0,0 +1,120 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.comptabilite.TypeCompteComptable; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité CompteComptable pour le plan comptable + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "comptes_comptables", + indexes = { + @Index(name = "idx_compte_numero", columnList = "numero_compte", unique = true), + @Index(name = "idx_compte_type", columnList = "type_compte"), + @Index(name = "idx_compte_classe", columnList = "classe_comptable") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class CompteComptable extends BaseEntity { + + /** Numéro de compte unique (ex: 411000, 512000) */ + @NotBlank + @Column(name = "numero_compte", unique = true, nullable = false, length = 10) + private String numeroCompte; + + /** Libellé du compte */ + @NotBlank + @Column(name = "libelle", nullable = false, length = 200) + private String libelle; + + /** Type de compte */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "type_compte", nullable = false, length = 30) + private TypeCompteComptable typeCompte; + + /** Classe comptable (1-7) */ + @NotNull + @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") + @Column(name = "classe_comptable", nullable = false) + private Integer classeComptable; + + /** Solde initial */ + @Builder.Default + @DecimalMin(value = "0.0", message = "Le solde initial doit être positif ou nul") + @Digits(integer = 12, fraction = 2) + @Column(name = "solde_initial", precision = 14, scale = 2) + private BigDecimal soldeInitial = BigDecimal.ZERO; + + /** Solde actuel (calculé) */ + @Builder.Default + @Digits(integer = 12, fraction = 2) + @Column(name = "solde_actuel", precision = 14, scale = 2) + private BigDecimal soldeActuel = BigDecimal.ZERO; + + /** Compte collectif (regroupe plusieurs sous-comptes) */ + @Builder.Default + @Column(name = "compte_collectif", nullable = false) + private Boolean compteCollectif = false; + + /** Compte analytique */ + @Builder.Default + @Column(name = "compte_analytique", nullable = false) + private Boolean compteAnalytique = false; + + /** Description du compte */ + @Column(name = "description", length = 500) + private String description; + + /** Lignes d'écriture associées */ + @OneToMany(mappedBy = "compteComptable", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List lignesEcriture = new ArrayList<>(); + + /** Méthode métier pour obtenir le numéro formaté */ + public String getNumeroFormate() { + return String.format("%-10s", numeroCompte); + } + + /** Méthode métier pour vérifier si c'est un compte de trésorerie */ + public boolean isTresorerie() { + return TypeCompteComptable.TRESORERIE.equals(typeCompte); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (soldeInitial == null) { + soldeInitial = BigDecimal.ZERO; + } + if (soldeActuel == null) { + soldeActuel = soldeInitial; + } + if (compteCollectif == null) { + compteCollectif = false; + } + if (compteAnalytique == null) { + compteAnalytique = false; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/CompteWave.java b/src/main/java/dev/lions/unionflow/server/entity/CompteWave.java new file mode 100644 index 0000000..5aa1293 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/CompteWave.java @@ -0,0 +1,107 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.wave.StatutCompteWave; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité CompteWave pour la gestion des comptes Wave Mobile Money + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "comptes_wave", + indexes = { + @Index(name = "idx_compte_wave_telephone", columnList = "numero_telephone", unique = true), + @Index(name = "idx_compte_wave_statut", columnList = "statut_compte"), + @Index(name = "idx_compte_wave_organisation", columnList = "organisation_id"), + @Index(name = "idx_compte_wave_membre", columnList = "membre_id") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class CompteWave extends BaseEntity { + + /** Numéro de téléphone Wave (format +225XXXXXXXX) */ + @NotBlank + @Pattern( + regexp = "^\\+225[0-9]{8}$", + message = "Le numéro de téléphone Wave doit être au format +225XXXXXXXX") + @Column(name = "numero_telephone", unique = true, nullable = false, length = 13) + private String numeroTelephone; + + /** Statut du compte */ + @Enumerated(EnumType.STRING) + @Builder.Default + @Column(name = "statut_compte", nullable = false, length = 30) + private StatutCompteWave statutCompte = StatutCompteWave.NON_VERIFIE; + + /** Identifiant Wave API (encrypté) */ + @Column(name = "wave_account_id", length = 255) + private String waveAccountId; + + /** Clé API Wave (encryptée) */ + @Column(name = "wave_api_key", length = 500) + private String waveApiKey; + + /** Environnement (SANDBOX ou PRODUCTION) */ + @Column(name = "environnement", length = 20) + private String environnement; + + /** Date de dernière vérification */ + @Column(name = "date_derniere_verification") + private java.time.LocalDateTime dateDerniereVerification; + + /** Commentaires */ + @Column(name = "commentaire", length = 500) + private String commentaire; + + // Relations + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id") + private Organisation organisation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id") + private Membre membre; + + @OneToMany(mappedBy = "compteWave", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List transactions = new ArrayList<>(); + + /** Méthode métier pour vérifier si le compte est vérifié */ + public boolean isVerifie() { + return StatutCompteWave.VERIFIE.equals(statutCompte); + } + + /** Méthode métier pour vérifier si le compte peut être utilisé */ + public boolean peutEtreUtilise() { + return StatutCompteWave.VERIFIE.equals(statutCompte); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (statutCompte == null) { + statutCompte = StatutCompteWave.NON_VERIFIE; + } + if (environnement == null || environnement.isEmpty()) { + environnement = "SANDBOX"; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/ConfigurationWave.java b/src/main/java/dev/lions/unionflow/server/entity/ConfigurationWave.java new file mode 100644 index 0000000..6adca19 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/ConfigurationWave.java @@ -0,0 +1,69 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité ConfigurationWave pour la configuration de l'intégration Wave + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "configurations_wave", + indexes = { + @Index(name = "idx_config_wave_cle", columnList = "cle", unique = true) + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class ConfigurationWave extends BaseEntity { + + /** Clé de configuration */ + @NotBlank + @Column(name = "cle", unique = true, nullable = false, length = 100) + private String cle; + + /** Valeur de configuration (peut être encryptée) */ + @Column(name = "valeur", columnDefinition = "TEXT") + private String valeur; + + /** Description de la configuration */ + @Column(name = "description", length = 500) + private String description; + + /** Type de valeur (STRING, NUMBER, BOOLEAN, JSON, ENCRYPTED) */ + @Column(name = "type_valeur", length = 20) + private String typeValeur; + + /** Environnement (SANDBOX, PRODUCTION, COMMON) */ + @Column(name = "environnement", length = 20) + private String environnement; + + /** Méthode métier pour vérifier si la valeur est encryptée */ + public boolean isEncryptee() { + return "ENCRYPTED".equals(typeValeur); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (typeValeur == null || typeValeur.isEmpty()) { + typeValeur = "STRING"; + } + if (environnement == null || environnement.isEmpty()) { + environnement = "COMMON"; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/Cotisation.java b/src/main/java/dev/lions/unionflow/server/entity/Cotisation.java new file mode 100644 index 0000000..a157083 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Cotisation.java @@ -0,0 +1,184 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Cotisation avec UUID Représente une cotisation d'un membre à son organisation + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +@Entity +@Table( + name = "cotisations", + indexes = { + @Index(name = "idx_cotisation_membre", columnList = "membre_id"), + @Index(name = "idx_cotisation_reference", columnList = "numero_reference", unique = true), + @Index(name = "idx_cotisation_statut", columnList = "statut"), + @Index(name = "idx_cotisation_echeance", columnList = "date_echeance"), + @Index(name = "idx_cotisation_type", columnList = "type_cotisation"), + @Index(name = "idx_cotisation_annee_mois", columnList = "annee, mois") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Cotisation extends BaseEntity { + + @NotBlank + @Column(name = "numero_reference", unique = true, nullable = false, length = 50) + private String numeroReference; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id", nullable = false) + private Membre membre; + + @NotBlank + @Column(name = "type_cotisation", nullable = false, length = 50) + private String typeCotisation; + + @NotNull + @DecimalMin(value = "0.0", message = "Le montant dû doit être positif") + @Digits(integer = 10, fraction = 2) + @Column(name = "montant_du", nullable = false, precision = 12, scale = 2) + private BigDecimal montantDu; + + @Builder.Default + @DecimalMin(value = "0.0", message = "Le montant payé doit être positif") + @Digits(integer = 10, fraction = 2) + @Column(name = "montant_paye", nullable = false, precision = 12, scale = 2) + private BigDecimal montantPaye = BigDecimal.ZERO; + + @NotBlank + @Pattern(regexp = "^[A-Z]{3}$", message = "Le code devise doit être un code ISO à 3 lettres") + @Column(name = "code_devise", nullable = false, length = 3) + private String codeDevise; + + @NotBlank + @Pattern(regexp = "^(EN_ATTENTE|PAYEE|EN_RETARD|PARTIELLEMENT_PAYEE|ANNULEE)$") + @Column(name = "statut", nullable = false, length = 30) + private String statut; + + @NotNull + @Column(name = "date_echeance", nullable = false) + private LocalDate dateEcheance; + + @Column(name = "date_paiement") + private LocalDateTime datePaiement; + + @Size(max = 500) + @Column(name = "description", length = 500) + private String description; + + @Size(max = 20) + @Column(name = "periode", length = 20) + private String periode; + + @NotNull + @Min(value = 2020, message = "L'année doit être supérieure à 2020") + @Max(value = 2100, message = "L'année doit être inférieure à 2100") + @Column(name = "annee", nullable = false) + private Integer annee; + + @Min(value = 1, message = "Le mois doit être entre 1 et 12") + @Max(value = 12, message = "Le mois doit être entre 1 et 12") + @Column(name = "mois") + private Integer mois; + + @Size(max = 1000) + @Column(name = "observations", length = 1000) + private String observations; + + @Builder.Default + @Column(name = "recurrente", nullable = false) + private Boolean recurrente = false; + + @Builder.Default + @Min(value = 0, message = "Le nombre de rappels doit être positif") + @Column(name = "nombre_rappels", nullable = false) + private Integer nombreRappels = 0; + + @Column(name = "date_dernier_rappel") + private LocalDateTime dateDernierRappel; + + @Column(name = "valide_par_id") + private UUID valideParId; + + @Size(max = 100) + @Column(name = "nom_validateur", length = 100) + private String nomValidateur; + + @Column(name = "date_validation") + private LocalDateTime dateValidation; + + @Size(max = 50) + @Column(name = "methode_paiement", length = 50) + private String methodePaiement; + + @Size(max = 100) + @Column(name = "reference_paiement", length = 100) + private String referencePaiement; + + /** Méthode métier pour calculer le montant restant à payer */ + public BigDecimal getMontantRestant() { + if (montantDu == null || montantPaye == null) { + return BigDecimal.ZERO; + } + return montantDu.subtract(montantPaye); + } + + /** Méthode métier pour vérifier si la cotisation est entièrement payée */ + public boolean isEntierementPayee() { + return getMontantRestant().compareTo(BigDecimal.ZERO) <= 0; + } + + /** Méthode métier pour vérifier si la cotisation est en retard */ + public boolean isEnRetard() { + return dateEcheance != null && dateEcheance.isBefore(LocalDate.now()) && !isEntierementPayee(); + } + + /** Méthode métier pour générer un numéro de référence unique */ + public static String genererNumeroReference() { + return "COT-" + + LocalDate.now().getYear() + + "-" + + String.format("%08d", System.currentTimeMillis() % 100000000); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); // Appelle le onCreate de BaseEntity + if (numeroReference == null || numeroReference.isEmpty()) { + numeroReference = genererNumeroReference(); + } + if (codeDevise == null) { + codeDevise = "XOF"; + } + if (statut == null) { + statut = "EN_ATTENTE"; + } + if (montantPaye == null) { + montantPaye = BigDecimal.ZERO; + } + if (nombreRappels == null) { + nombreRappels = 0; + } + if (recurrente == null) { + recurrente = false; + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/entity/DemandeAide.java b/src/main/java/dev/lions/unionflow/server/entity/DemandeAide.java new file mode 100644 index 0000000..5e6994c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/DemandeAide.java @@ -0,0 +1,130 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** Entité représentant une demande d'aide dans le système de solidarité */ +@Entity +@Table(name = "demandes_aide") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class DemandeAide extends BaseEntity { + + @Column(name = "titre", nullable = false, length = 200) + private String titre; + + @Column(name = "description", nullable = false, columnDefinition = "TEXT") + private String description; + + @Enumerated(EnumType.STRING) + @Column(name = "type_aide", nullable = false) + private TypeAide typeAide; + + @Enumerated(EnumType.STRING) + @Column(name = "statut", nullable = false) + private StatutAide statut; + + @Column(name = "montant_demande", precision = 10, scale = 2) + private BigDecimal montantDemande; + + @Column(name = "montant_approuve", precision = 10, scale = 2) + private BigDecimal montantApprouve; + + @Column(name = "date_demande", nullable = false) + private LocalDateTime dateDemande; + + @Column(name = "date_evaluation") + private LocalDateTime dateEvaluation; + + @Column(name = "date_versement") + private LocalDateTime dateVersement; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "demandeur_id", nullable = false) + private Membre demandeur; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "evaluateur_id") + private Membre evaluateur; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id", nullable = false) + private Organisation organisation; + + @Column(name = "justification", columnDefinition = "TEXT") + private String justification; + + @Column(name = "commentaire_evaluation", columnDefinition = "TEXT") + private String commentaireEvaluation; + + @Column(name = "urgence", nullable = false) + @Builder.Default + private Boolean urgence = false; + + @Column(name = "documents_fournis") + private String documentsFournis; + + @PrePersist + protected void onCreate() { + super.onCreate(); // Appelle le onCreate de BaseEntity + if (dateDemande == null) { + dateDemande = LocalDateTime.now(); + } + if (statut == null) { + statut = StatutAide.EN_ATTENTE; + } + if (urgence == null) { + urgence = false; + } + } + + @PreUpdate + protected void onUpdate() { + // Méthode appelée avant mise à jour + } + + /** Vérifie si la demande est en attente */ + public boolean isEnAttente() { + return StatutAide.EN_ATTENTE.equals(statut); + } + + /** Vérifie si la demande est approuvée */ + public boolean isApprouvee() { + return StatutAide.APPROUVEE.equals(statut); + } + + /** Vérifie si la demande est rejetée */ + public boolean isRejetee() { + return StatutAide.REJETEE.equals(statut); + } + + /** Vérifie si la demande est urgente */ + public boolean isUrgente() { + return Boolean.TRUE.equals(urgence); + } + + /** Calcule le pourcentage d'approbation par rapport au montant demandé */ + public BigDecimal getPourcentageApprobation() { + if (montantDemande == null || montantDemande.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + if (montantApprouve == null) { + return BigDecimal.ZERO; + } + return montantApprouve + .divide(montantDemande, 4, RoundingMode.HALF_UP) + .multiply(BigDecimal.valueOf(100)); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/entity/Document.java b/src/main/java/dev/lions/unionflow/server/entity/Document.java new file mode 100644 index 0000000..063c69e --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Document.java @@ -0,0 +1,128 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.document.TypeDocument; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Document pour la gestion documentaire sécurisée + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "documents", + indexes = { + @Index(name = "idx_document_nom_fichier", columnList = "nom_fichier"), + @Index(name = "idx_document_type", columnList = "type_document"), + @Index(name = "idx_document_hash_md5", columnList = "hash_md5"), + @Index(name = "idx_document_hash_sha256", columnList = "hash_sha256") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Document extends BaseEntity { + + /** Nom du fichier original */ + @NotBlank + @Column(name = "nom_fichier", nullable = false, length = 255) + private String nomFichier; + + /** Nom original du fichier (tel que téléchargé) */ + @Column(name = "nom_original", length = 255) + private String nomOriginal; + + /** Chemin de stockage */ + @NotBlank + @Column(name = "chemin_stockage", nullable = false, length = 1000) + private String cheminStockage; + + /** Type MIME */ + @Column(name = "type_mime", length = 100) + private String typeMime; + + /** Taille du fichier en octets */ + @NotNull + @Min(value = 0, message = "La taille doit être positive") + @Column(name = "taille_octets", nullable = false) + private Long tailleOctets; + + /** Type de document */ + @Enumerated(EnumType.STRING) + @Column(name = "type_document", length = 50) + private TypeDocument typeDocument; + + /** Hash MD5 pour vérification d'intégrité */ + @Column(name = "hash_md5", length = 32) + private String hashMd5; + + /** Hash SHA256 pour vérification d'intégrité */ + @Column(name = "hash_sha256", length = 64) + private String hashSha256; + + /** Description du document */ + @Column(name = "description", length = 1000) + private String description; + + /** Nombre de téléchargements */ + @Builder.Default + @Column(name = "nombre_telechargements", nullable = false) + private Integer nombreTelechargements = 0; + + /** Date de dernier téléchargement */ + @Column(name = "date_dernier_telechargement") + private java.time.LocalDateTime dateDernierTelechargement; + + /** Pièces jointes associées */ + @OneToMany(mappedBy = "document", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List piecesJointes = new ArrayList<>(); + + /** Méthode métier pour vérifier l'intégrité avec MD5 */ + public boolean verifierIntegriteMd5(String hashAttendu) { + return hashMd5 != null && hashMd5.equalsIgnoreCase(hashAttendu); + } + + /** Méthode métier pour vérifier l'intégrité avec SHA256 */ + public boolean verifierIntegriteSha256(String hashAttendu) { + return hashSha256 != null && hashSha256.equalsIgnoreCase(hashAttendu); + } + + /** Méthode métier pour obtenir la taille formatée */ + public String getTailleFormatee() { + if (tailleOctets == null) { + return "0 B"; + } + if (tailleOctets < 1024) { + return tailleOctets + " B"; + } else if (tailleOctets < 1024 * 1024) { + return String.format("%.2f KB", tailleOctets / 1024.0); + } else { + return String.format("%.2f MB", tailleOctets / (1024.0 * 1024.0)); + } + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (nombreTelechargements == null) { + nombreTelechargements = 0; + } + if (typeDocument == null) { + typeDocument = TypeDocument.AUTRE; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/EcritureComptable.java b/src/main/java/dev/lions/unionflow/server/entity/EcritureComptable.java new file mode 100644 index 0000000..940f6de --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/EcritureComptable.java @@ -0,0 +1,172 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité EcritureComptable pour les écritures comptables + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "ecritures_comptables", + indexes = { + @Index(name = "idx_ecriture_numero_piece", columnList = "numero_piece", unique = true), + @Index(name = "idx_ecriture_date", columnList = "date_ecriture"), + @Index(name = "idx_ecriture_journal", columnList = "journal_id"), + @Index(name = "idx_ecriture_organisation", columnList = "organisation_id"), + @Index(name = "idx_ecriture_paiement", columnList = "paiement_id") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class EcritureComptable extends BaseEntity { + + /** Numéro de pièce unique */ + @NotBlank + @Column(name = "numero_piece", unique = true, nullable = false, length = 50) + private String numeroPiece; + + /** Date de l'écriture */ + @NotNull + @Column(name = "date_ecriture", nullable = false) + private LocalDate dateEcriture; + + /** Libellé de l'écriture */ + @NotBlank + @Column(name = "libelle", nullable = false, length = 500) + private String libelle; + + /** Référence externe */ + @Column(name = "reference", length = 100) + private String reference; + + /** Lettrage (pour rapprochement) */ + @Column(name = "lettrage", length = 20) + private String lettrage; + + /** Pointage (pour rapprochement bancaire) */ + @Builder.Default + @Column(name = "pointe", nullable = false) + private Boolean pointe = false; + + /** Montant total débit (somme des lignes) */ + @Builder.Default + @DecimalMin(value = "0.0") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_debit", precision = 14, scale = 2) + private BigDecimal montantDebit = BigDecimal.ZERO; + + /** Montant total crédit (somme des lignes) */ + @Builder.Default + @DecimalMin(value = "0.0") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_credit", precision = 14, scale = 2) + private BigDecimal montantCredit = BigDecimal.ZERO; + + /** Commentaires */ + @Column(name = "commentaire", length = 1000) + private String commentaire; + + // Relations + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "journal_id", nullable = false) + private JournalComptable journal; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id") + private Organisation organisation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "paiement_id") + private Paiement paiement; + + /** Lignes d'écriture */ + @OneToMany(mappedBy = "ecriture", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) + @Builder.Default + private List lignes = new ArrayList<>(); + + /** Méthode métier pour vérifier l'équilibre (Débit = Crédit) */ + public boolean isEquilibree() { + if (montantDebit == null || montantCredit == null) { + return false; + } + return montantDebit.compareTo(montantCredit) == 0; + } + + /** Méthode métier pour calculer les totaux à partir des lignes */ + public void calculerTotaux() { + if (lignes == null || lignes.isEmpty()) { + montantDebit = BigDecimal.ZERO; + montantCredit = BigDecimal.ZERO; + return; + } + + montantDebit = + lignes.stream() + .map(LigneEcriture::getMontantDebit) + .filter(amount -> amount != null) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + montantCredit = + lignes.stream() + .map(LigneEcriture::getMontantCredit) + .filter(amount -> amount != null) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + /** Méthode métier pour générer un numéro de pièce unique */ + public static String genererNumeroPiece(String prefixe, LocalDate date) { + return String.format( + "%s-%04d%02d%02d-%012d", + prefixe, date.getYear(), date.getMonthValue(), date.getDayOfMonth(), + System.currentTimeMillis() % 1000000000000L); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (numeroPiece == null || numeroPiece.isEmpty()) { + numeroPiece = genererNumeroPiece("ECR", dateEcriture != null ? dateEcriture : LocalDate.now()); + } + if (dateEcriture == null) { + dateEcriture = LocalDate.now(); + } + if (montantDebit == null) { + montantDebit = BigDecimal.ZERO; + } + if (montantCredit == null) { + montantCredit = BigDecimal.ZERO; + } + if (pointe == null) { + pointe = false; + } + // Calculer les totaux si les lignes sont déjà présentes + if (lignes != null && !lignes.isEmpty()) { + calculerTotaux(); + } + } + + /** Callback JPA avant la mise à jour */ + @PreUpdate + protected void onUpdate() { + calculerTotaux(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/Evenement.java b/src/main/java/dev/lions/unionflow/server/entity/Evenement.java new file mode 100644 index 0000000..5f1ccc1 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Evenement.java @@ -0,0 +1,267 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import lombok.*; + +/** + * Entité Événement pour la gestion des événements de l'union + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +@Entity +@Table( + name = "evenements", + indexes = { + @Index(name = "idx_evenement_date_debut", columnList = "date_debut"), + @Index(name = "idx_evenement_statut", columnList = "statut"), + @Index(name = "idx_evenement_type", columnList = "type_evenement"), + @Index(name = "idx_evenement_organisation", columnList = "organisation_id") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Evenement extends BaseEntity { + + @NotBlank + @Size(min = 3, max = 200) + @Column(name = "titre", nullable = false, length = 200) + private String titre; + + @Size(max = 2000) + @Column(name = "description", length = 2000) + private String description; + + @NotNull + @Column(name = "date_debut", nullable = false) + private LocalDateTime dateDebut; + + @Column(name = "date_fin") + private LocalDateTime dateFin; + + @Size(max = 500) + @Column(name = "lieu", length = 500) + private String lieu; + + @Size(max = 1000) + @Column(name = "adresse", length = 1000) + private String adresse; + + @Enumerated(EnumType.STRING) + @Column(name = "type_evenement", length = 50) + private TypeEvenement typeEvenement; + + @Enumerated(EnumType.STRING) + @Builder.Default + @Column(name = "statut", nullable = false, length = 30) + private StatutEvenement statut = StatutEvenement.PLANIFIE; + + @Min(0) + @Column(name = "capacite_max") + private Integer capaciteMax; + + @DecimalMin("0.00") + @Digits(integer = 8, fraction = 2) + @Column(name = "prix", precision = 10, scale = 2) + private BigDecimal prix; + + @Builder.Default + @Column(name = "inscription_requise", nullable = false) + private Boolean inscriptionRequise = false; + + @Column(name = "date_limite_inscription") + private LocalDateTime dateLimiteInscription; + + @Size(max = 1000) + @Column(name = "instructions_particulieres", length = 1000) + private String instructionsParticulieres; + + @Size(max = 500) + @Column(name = "contact_organisateur", length = 500) + private String contactOrganisateur; + + @Size(max = 2000) + @Column(name = "materiel_requis", length = 2000) + private String materielRequis; + + @Builder.Default + @Column(name = "visible_public", nullable = false) + private Boolean visiblePublic = true; + + @Builder.Default + @Column(name = "actif", nullable = false) + private Boolean actif = true; + + // Relations + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id") + private Organisation organisation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisateur_id") + private Membre organisateur; + + @OneToMany( + mappedBy = "evenement", + cascade = CascadeType.ALL, + orphanRemoval = true, + fetch = FetchType.LAZY) + @Builder.Default + private List inscriptions = new ArrayList<>(); + + @OneToMany(mappedBy = "evenement", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List adresses = new ArrayList<>(); + + /** Types d'événements */ + public enum TypeEvenement { + ASSEMBLEE_GENERALE("Assemblée Générale"), + REUNION("Réunion"), + FORMATION("Formation"), + CONFERENCE("Conférence"), + ATELIER("Atelier"), + SEMINAIRE("Séminaire"), + EVENEMENT_SOCIAL("Événement Social"), + MANIFESTATION("Manifestation"), + CELEBRATION("Célébration"), + AUTRE("Autre"); + + private final String libelle; + + TypeEvenement(String libelle) { + this.libelle = libelle; + } + + public String getLibelle() { + return libelle; + } + } + + /** Statuts d'événement */ + public enum StatutEvenement { + PLANIFIE("Planifié"), + CONFIRME("Confirmé"), + EN_COURS("En cours"), + TERMINE("Terminé"), + ANNULE("Annulé"), + REPORTE("Reporté"); + + private final String libelle; + + StatutEvenement(String libelle) { + this.libelle = libelle; + } + + public String getLibelle() { + return libelle; + } + } + + // Méthodes métier + + /** Vérifie si l'événement est ouvert aux inscriptions */ + public boolean isOuvertAuxInscriptions() { + if (!inscriptionRequise || !actif) { + return false; + } + + LocalDateTime maintenant = LocalDateTime.now(); + + // Vérifier si la date limite d'inscription n'est pas dépassée + if (dateLimiteInscription != null && maintenant.isAfter(dateLimiteInscription)) { + return false; + } + + // Vérifier si l'événement n'a pas déjà commencé + if (maintenant.isAfter(dateDebut)) { + return false; + } + + // Vérifier la capacité + if (capaciteMax != null && getNombreInscrits() >= capaciteMax) { + return false; + } + + return statut == StatutEvenement.PLANIFIE || statut == StatutEvenement.CONFIRME; + } + + /** Obtient le nombre d'inscrits à l'événement */ + public int getNombreInscrits() { + return inscriptions != null + ? (int) + inscriptions.stream() + .filter( + inscription -> + inscription.getStatut() == InscriptionEvenement.StatutInscription.CONFIRMEE) + .count() + : 0; + } + + /** Vérifie si l'événement est complet */ + public boolean isComplet() { + return capaciteMax != null && getNombreInscrits() >= capaciteMax; + } + + /** Vérifie si l'événement est en cours */ + public boolean isEnCours() { + LocalDateTime maintenant = LocalDateTime.now(); + return maintenant.isAfter(dateDebut) && (dateFin == null || maintenant.isBefore(dateFin)); + } + + /** Vérifie si l'événement est terminé */ + public boolean isTermine() { + if (statut == StatutEvenement.TERMINE) { + return true; + } + + LocalDateTime maintenant = LocalDateTime.now(); + return dateFin != null && maintenant.isAfter(dateFin); + } + + /** Calcule la durée de l'événement en heures */ + public Long getDureeEnHeures() { + if (dateFin == null) { + return null; + } + + return java.time.Duration.between(dateDebut, dateFin).toHours(); + } + + /** Obtient le nombre de places restantes */ + public Integer getPlacesRestantes() { + if (capaciteMax == null) { + return null; // Capacité illimitée + } + + return Math.max(0, capaciteMax - getNombreInscrits()); + } + + /** Vérifie si un membre est inscrit à l'événement */ + public boolean isMemberInscrit(UUID membreId) { + return inscriptions != null + && inscriptions.stream() + .anyMatch( + inscription -> + inscription.getMembre().getId().equals(membreId) + && inscription.getStatut() + == InscriptionEvenement.StatutInscription.CONFIRMEE); + } + + /** Obtient le taux de remplissage en pourcentage */ + public Double getTauxRemplissage() { + if (capaciteMax == null || capaciteMax == 0) { + return null; + } + + return (double) getNombreInscrits() / capaciteMax * 100; + } +} diff --git a/src/main/java/dev/lions/unionflow/server/entity/InscriptionEvenement.java b/src/main/java/dev/lions/unionflow/server/entity/InscriptionEvenement.java new file mode 100644 index 0000000..0cec9c7 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/InscriptionEvenement.java @@ -0,0 +1,156 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.*; + +/** + * Entité InscriptionEvenement représentant l'inscription d'un membre à un événement + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +@Entity +@Table( + name = "inscriptions_evenement", + indexes = { + @Index(name = "idx_inscription_membre", columnList = "membre_id"), + @Index(name = "idx_inscription_evenement", columnList = "evenement_id"), + @Index(name = "idx_inscription_date", columnList = "date_inscription") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class InscriptionEvenement extends BaseEntity { + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id", nullable = false) + private Membre membre; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "evenement_id", nullable = false) + private Evenement evenement; + + @Builder.Default + @Column(name = "date_inscription", nullable = false) + private LocalDateTime dateInscription = LocalDateTime.now(); + + @Enumerated(EnumType.STRING) + @Column(name = "statut", length = 20) + @Builder.Default + private StatutInscription statut = StatutInscription.CONFIRMEE; + + @Column(name = "commentaire", length = 500) + private String commentaire; + + /** Énumération des statuts d'inscription */ + public enum StatutInscription { + CONFIRMEE("Confirmée"), + EN_ATTENTE("En attente"), + ANNULEE("Annulée"), + REFUSEE("Refusée"); + + private final String libelle; + + StatutInscription(String libelle) { + this.libelle = libelle; + } + + public String getLibelle() { + return libelle; + } + } + + // Méthodes utilitaires + + /** + * Vérifie si l'inscription est confirmée + * + * @return true si l'inscription est confirmée + */ + public boolean isConfirmee() { + return StatutInscription.CONFIRMEE.equals(this.statut); + } + + /** + * Vérifie si l'inscription est en attente + * + * @return true si l'inscription est en attente + */ + public boolean isEnAttente() { + return StatutInscription.EN_ATTENTE.equals(this.statut); + } + + /** + * Vérifie si l'inscription est annulée + * + * @return true si l'inscription est annulée + */ + public boolean isAnnulee() { + return StatutInscription.ANNULEE.equals(this.statut); + } + + /** Confirme l'inscription */ + public void confirmer() { + this.statut = StatutInscription.CONFIRMEE; + this.dateModification = LocalDateTime.now(); + } + + /** + * Annule l'inscription + * + * @param commentaire le commentaire d'annulation + */ + public void annuler(String commentaire) { + this.statut = StatutInscription.ANNULEE; + this.commentaire = commentaire; + this.dateModification = LocalDateTime.now(); + } + + /** + * Met l'inscription en attente + * + * @param commentaire le commentaire de mise en attente + */ + public void mettreEnAttente(String commentaire) { + this.statut = StatutInscription.EN_ATTENTE; + this.commentaire = commentaire; + this.dateModification = LocalDateTime.now(); + } + + /** + * Refuse l'inscription + * + * @param commentaire le commentaire de refus + */ + public void refuser(String commentaire) { + this.statut = StatutInscription.REFUSEE; + this.commentaire = commentaire; + this.dateModification = LocalDateTime.now(); + } + + // Callbacks JPA + + @PreUpdate + public void preUpdate() { + super.onUpdate(); // Appelle le onUpdate de BaseEntity + this.dateModification = LocalDateTime.now(); + } + + @Override + public String toString() { + return String.format( + "InscriptionEvenement{id=%s, membre=%s, evenement=%s, statut=%s, dateInscription=%s}", + getId(), + membre != null ? membre.getEmail() : "null", + evenement != null ? evenement.getTitre() : "null", + statut, + dateInscription); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/entity/JournalComptable.java b/src/main/java/dev/lions/unionflow/server/entity/JournalComptable.java new file mode 100644 index 0000000..cc4109c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/JournalComptable.java @@ -0,0 +1,98 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.comptabilite.TypeJournalComptable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité JournalComptable pour la gestion des journaux + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "journaux_comptables", + indexes = { + @Index(name = "idx_journal_code", columnList = "code", unique = true), + @Index(name = "idx_journal_type", columnList = "type_journal"), + @Index(name = "idx_journal_periode", columnList = "date_debut, date_fin") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class JournalComptable extends BaseEntity { + + /** Code unique du journal */ + @NotBlank + @Column(name = "code", unique = true, nullable = false, length = 10) + private String code; + + /** Libellé du journal */ + @NotBlank + @Column(name = "libelle", nullable = false, length = 100) + private String libelle; + + /** Type de journal */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "type_journal", nullable = false, length = 30) + private TypeJournalComptable typeJournal; + + /** Date de début de la période */ + @Column(name = "date_debut") + private LocalDate dateDebut; + + /** Date de fin de la période */ + @Column(name = "date_fin") + private LocalDate dateFin; + + /** Statut du journal (OUVERT, FERME, ARCHIVE) */ + @Builder.Default + @Column(name = "statut", length = 20) + private String statut = "OUVERT"; + + /** Description */ + @Column(name = "description", length = 500) + private String description; + + /** Écritures comptables associées */ + @OneToMany(mappedBy = "journal", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List ecritures = new ArrayList<>(); + + /** Méthode métier pour vérifier si le journal est ouvert */ + public boolean isOuvert() { + return "OUVERT".equals(statut); + } + + /** Méthode métier pour vérifier si une date est dans la période */ + public boolean estDansPeriode(LocalDate date) { + if (dateDebut == null || dateFin == null) { + return true; // Période illimitée + } + return !date.isBefore(dateDebut) && !date.isAfter(dateFin); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (statut == null || statut.isEmpty()) { + statut = "OUVERT"; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/LigneEcriture.java b/src/main/java/dev/lions/unionflow/server/entity/LigneEcriture.java new file mode 100644 index 0000000..08a170c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/LigneEcriture.java @@ -0,0 +1,100 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité LigneEcriture pour les lignes d'une écriture comptable + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "lignes_ecriture", + indexes = { + @Index(name = "idx_ligne_ecriture_ecriture", columnList = "ecriture_id"), + @Index(name = "idx_ligne_ecriture_compte", columnList = "compte_comptable_id") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class LigneEcriture extends BaseEntity { + + /** Numéro de ligne */ + @NotNull + @Min(value = 1, message = "Le numéro de ligne doit être positif") + @Column(name = "numero_ligne", nullable = false) + private Integer numeroLigne; + + /** Montant débit */ + @DecimalMin(value = "0.0", message = "Le montant débit doit être positif ou nul") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_debit", precision = 14, scale = 2) + private BigDecimal montantDebit; + + /** Montant crédit */ + @DecimalMin(value = "0.0", message = "Le montant crédit doit être positif ou nul") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_credit", precision = 14, scale = 2) + private BigDecimal montantCredit; + + /** Libellé de la ligne */ + @Column(name = "libelle", length = 500) + private String libelle; + + /** Référence */ + @Column(name = "reference", length = 100) + private String reference; + + // Relations + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ecriture_id", nullable = false) + private EcritureComptable ecriture; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "compte_comptable_id", nullable = false) + private CompteComptable compteComptable; + + /** Méthode métier pour vérifier que la ligne a soit un débit soit un crédit (pas les deux) */ + public boolean isValide() { + boolean aDebit = montantDebit != null && montantDebit.compareTo(BigDecimal.ZERO) > 0; + boolean aCredit = montantCredit != null && montantCredit.compareTo(BigDecimal.ZERO) > 0; + return aDebit != aCredit; // XOR : soit débit, soit crédit, pas les deux + } + + /** Méthode métier pour obtenir le montant (débit ou crédit) */ + public BigDecimal getMontant() { + if (montantDebit != null && montantDebit.compareTo(BigDecimal.ZERO) > 0) { + return montantDebit; + } + if (montantCredit != null && montantCredit.compareTo(BigDecimal.ZERO) > 0) { + return montantCredit; + } + return BigDecimal.ZERO; + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (montantDebit == null) { + montantDebit = BigDecimal.ZERO; + } + if (montantCredit == null) { + montantCredit = BigDecimal.ZERO; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/Membre.java b/src/main/java/dev/lions/unionflow/server/entity/Membre.java new file mode 100644 index 0000000..8f943a1 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Membre.java @@ -0,0 +1,106 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** Entité Membre avec UUID */ +@Entity +@Table( + name = "membres", + indexes = { + @Index(name = "idx_membre_email", columnList = "email", unique = true), + @Index(name = "idx_membre_numero", columnList = "numero_membre", unique = true), + @Index(name = "idx_membre_actif", columnList = "actif") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Membre extends BaseEntity { + + @NotBlank + @Column(name = "numero_membre", unique = true, nullable = false, length = 20) + private String numeroMembre; + + @NotBlank + @Column(name = "prenom", nullable = false, length = 100) + private String prenom; + + @NotBlank + @Column(name = "nom", nullable = false, length = 100) + private String nom; + + @Email + @NotBlank + @Column(name = "email", unique = true, nullable = false, length = 255) + private String email; + + @Column(name = "mot_de_passe", length = 255) + private String motDePasse; + + @Column(name = "telephone", length = 20) + private String telephone; + + @Pattern( + regexp = "^\\+225[0-9]{8}$", + message = "Le numéro de téléphone Wave doit être au format +225XXXXXXXX") + @Column(name = "telephone_wave", length = 13) + private String telephoneWave; + + @NotNull + @Column(name = "date_naissance", nullable = false) + private LocalDate dateNaissance; + + @NotNull + @Column(name = "date_adhesion", nullable = false) + private LocalDate dateAdhesion; + + // Relations + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id") + private Organisation organisation; + + @OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List adresses = new ArrayList<>(); + + @OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List roles = new ArrayList<>(); + + @OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List comptesWave = new ArrayList<>(); + + @OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List paiements = new ArrayList<>(); + + /** Méthode métier pour obtenir le nom complet */ + public String getNomComplet() { + return prenom + " " + nom; + } + + /** Méthode métier pour vérifier si le membre est majeur */ + public boolean isMajeur() { + return dateNaissance.isBefore(LocalDate.now().minusYears(18)); + } + + /** Méthode métier pour calculer l'âge */ + public int getAge() { + return LocalDate.now().getYear() - dateNaissance.getYear(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/entity/MembreRole.java b/src/main/java/dev/lions/unionflow/server/entity/MembreRole.java new file mode 100644 index 0000000..27f3025 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/MembreRole.java @@ -0,0 +1,88 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Table de liaison entre Membre et Role + * Permet à un membre d'avoir plusieurs rôles avec dates de début/fin + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "membres_roles", + indexes = { + @Index(name = "idx_membre_role_membre", columnList = "membre_id"), + @Index(name = "idx_membre_role_role", columnList = "role_id"), + @Index(name = "idx_membre_role_actif", columnList = "actif") + }, + uniqueConstraints = { + @UniqueConstraint( + name = "uk_membre_role", + columnNames = {"membre_id", "role_id"}) + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class MembreRole extends BaseEntity { + + /** Membre */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id", nullable = false) + private Membre membre; + + /** Rôle */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + /** Date de début d'attribution */ + @Column(name = "date_debut") + private LocalDate dateDebut; + + /** Date de fin d'attribution (null = permanent) */ + @Column(name = "date_fin") + private LocalDate dateFin; + + /** Commentaire sur l'attribution */ + @Column(name = "commentaire", length = 500) + private String commentaire; + + /** Méthode métier pour vérifier si l'attribution est active */ + public boolean isActif() { + if (!Boolean.TRUE.equals(getActif())) { + return false; + } + LocalDate aujourdhui = LocalDate.now(); + if (dateDebut != null && aujourdhui.isBefore(dateDebut)) { + return false; + } + if (dateFin != null && aujourdhui.isAfter(dateFin)) { + return false; + } + return true; + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (dateDebut == null) { + dateDebut = LocalDate.now(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/Notification.java b/src/main/java/dev/lions/unionflow/server/entity/Notification.java new file mode 100644 index 0000000..8170d4f --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Notification.java @@ -0,0 +1,132 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.notification.PrioriteNotification; +import dev.lions.unionflow.server.api.enums.notification.TypeNotification; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Notification pour la gestion des notifications + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "notifications", + indexes = { + @Index(name = "idx_notification_type", columnList = "type_notification"), + @Index(name = "idx_notification_statut", columnList = "statut"), + @Index(name = "idx_notification_priorite", columnList = "priorite"), + @Index(name = "idx_notification_membre", columnList = "membre_id"), + @Index(name = "idx_notification_organisation", columnList = "organisation_id"), + @Index(name = "idx_notification_date_envoi", columnList = "date_envoi") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Notification extends BaseEntity { + + /** Type de notification */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "type_notification", nullable = false, length = 30) + private TypeNotification typeNotification; + + /** Priorité */ + @Enumerated(EnumType.STRING) + @Builder.Default + @Column(name = "priorite", length = 20) + private PrioriteNotification priorite = PrioriteNotification.NORMALE; + + /** Statut */ + @Enumerated(EnumType.STRING) + @Builder.Default + @Column(name = "statut", length = 30) + private dev.lions.unionflow.server.api.enums.notification.StatutNotification statut = + dev.lions.unionflow.server.api.enums.notification.StatutNotification.EN_ATTENTE; + + /** Sujet */ + @Column(name = "sujet", length = 500) + private String sujet; + + /** Corps du message */ + @Column(name = "corps", columnDefinition = "TEXT") + private String corps; + + /** Date d'envoi prévue */ + @Column(name = "date_envoi_prevue") + private LocalDateTime dateEnvoiPrevue; + + /** Date d'envoi réelle */ + @Column(name = "date_envoi") + private LocalDateTime dateEnvoi; + + /** Date de lecture */ + @Column(name = "date_lecture") + private LocalDateTime dateLecture; + + /** Nombre de tentatives d'envoi */ + @Builder.Default + @Column(name = "nombre_tentatives", nullable = false) + private Integer nombreTentatives = 0; + + /** Message d'erreur (si échec) */ + @Column(name = "message_erreur", length = 1000) + private String messageErreur; + + /** Données additionnelles (JSON) */ + @Column(name = "donnees_additionnelles", columnDefinition = "TEXT") + private String donneesAdditionnelles; + + // Relations + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id") + private Membre membre; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id") + private Organisation organisation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "template_id") + private TemplateNotification template; + + /** Méthode métier pour vérifier si la notification est envoyée */ + public boolean isEnvoyee() { + return dev.lions.unionflow.server.api.enums.notification.StatutNotification.ENVOYEE.equals(statut); + } + + /** Méthode métier pour vérifier si la notification est lue */ + public boolean isLue() { + return dev.lions.unionflow.server.api.enums.notification.StatutNotification.LUE.equals(statut); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (priorite == null) { + priorite = PrioriteNotification.NORMALE; + } + if (statut == null) { + statut = dev.lions.unionflow.server.api.enums.notification.StatutNotification.EN_ATTENTE; + } + if (nombreTentatives == null) { + nombreTentatives = 0; + } + if (dateEnvoiPrevue == null) { + dateEnvoiPrevue = LocalDateTime.now(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/Organisation.java b/src/main/java/dev/lions/unionflow/server/entity/Organisation.java new file mode 100644 index 0000000..cd5eddd --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Organisation.java @@ -0,0 +1,308 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Period; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Organisation avec UUID Représente une organisation (Lions Club, Association, + * Coopérative, etc.) + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +@Entity +@Table( + name = "organisations", + indexes = { + @Index(name = "idx_organisation_nom", columnList = "nom"), + @Index(name = "idx_organisation_email", columnList = "email", unique = true), + @Index(name = "idx_organisation_statut", columnList = "statut"), + @Index(name = "idx_organisation_type", columnList = "type_organisation"), + @Index(name = "idx_organisation_ville", columnList = "ville"), + @Index(name = "idx_organisation_pays", columnList = "pays"), + @Index(name = "idx_organisation_parente", columnList = "organisation_parente_id"), + @Index( + name = "idx_organisation_numero_enregistrement", + columnList = "numero_enregistrement", + unique = true) + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Organisation extends BaseEntity { + + @NotBlank + @Column(name = "nom", nullable = false, length = 200) + private String nom; + + @Column(name = "nom_court", length = 50) + private String nomCourt; + + @NotBlank + @Column(name = "type_organisation", nullable = false, length = 50) + private String typeOrganisation; + + @NotBlank + @Column(name = "statut", nullable = false, length = 50) + private String statut; + + @Column(name = "description", length = 2000) + private String description; + + @Column(name = "date_fondation") + private LocalDate dateFondation; + + @Column(name = "numero_enregistrement", unique = true, length = 100) + private String numeroEnregistrement; + + // Informations de contact + @Email + @NotBlank + @Column(name = "email", unique = true, nullable = false, length = 255) + private String email; + + @Column(name = "telephone", length = 20) + private String telephone; + + @Column(name = "telephone_secondaire", length = 20) + private String telephoneSecondaire; + + @Email + @Column(name = "email_secondaire", length = 255) + private String emailSecondaire; + + // Adresse + @Column(name = "adresse", length = 500) + private String adresse; + + @Column(name = "ville", length = 100) + private String ville; + + @Column(name = "code_postal", length = 20) + private String codePostal; + + @Column(name = "region", length = 100) + private String region; + + @Column(name = "pays", length = 100) + private String pays; + + // Coordonnées géographiques + @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) + @Column(name = "latitude", precision = 9, scale = 6) + private 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) + @Column(name = "longitude", precision = 9, scale = 6) + private BigDecimal longitude; + + // Web et réseaux sociaux + @Column(name = "site_web", length = 500) + private String siteWeb; + + @Column(name = "logo", length = 500) + private String logo; + + @Column(name = "reseaux_sociaux", length = 1000) + private String reseauxSociaux; + + // Hiérarchie + @Column(name = "organisation_parente_id") + private UUID organisationParenteId; + + @Builder.Default + @Column(name = "niveau_hierarchique", nullable = false) + private Integer niveauHierarchique = 0; + + // Statistiques + @Builder.Default + @Column(name = "nombre_membres", nullable = false) + private Integer nombreMembres = 0; + + @Builder.Default + @Column(name = "nombre_administrateurs", nullable = false) + private Integer nombreAdministrateurs = 0; + + // Finances + @DecimalMin(value = "0.0", message = "Le budget annuel doit être positif") + @Digits(integer = 12, fraction = 2) + @Column(name = "budget_annuel", precision = 14, scale = 2) + private BigDecimal budgetAnnuel; + + @Builder.Default + @Column(name = "devise", length = 3) + private String devise = "XOF"; + + @Builder.Default + @Column(name = "cotisation_obligatoire", nullable = false) + private Boolean cotisationObligatoire = false; + + @DecimalMin(value = "0.0", message = "Le montant de cotisation doit être positif") + @Digits(integer = 10, fraction = 2) + @Column(name = "montant_cotisation_annuelle", precision = 12, scale = 2) + private BigDecimal montantCotisationAnnuelle; + + // Informations complémentaires + @Column(name = "objectifs", length = 2000) + private String objectifs; + + @Column(name = "activites_principales", length = 2000) + private String activitesPrincipales; + + @Column(name = "certifications", length = 500) + private String certifications; + + @Column(name = "partenaires", length = 1000) + private String partenaires; + + @Column(name = "notes", length = 1000) + private String notes; + + // Paramètres + @Builder.Default + @Column(name = "organisation_publique", nullable = false) + private Boolean organisationPublique = true; + + @Builder.Default + @Column(name = "accepte_nouveaux_membres", nullable = false) + private Boolean accepteNouveauxMembres = true; + + // Relations + @OneToMany(mappedBy = "organisation", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List membres = new ArrayList<>(); + + @OneToMany(mappedBy = "organisation", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List adresses = new ArrayList<>(); + + @OneToMany(mappedBy = "organisation", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List comptesWave = new ArrayList<>(); + + /** Méthode métier pour obtenir le nom complet avec sigle */ + public String getNomComplet() { + if (nomCourt != null && !nomCourt.isEmpty()) { + return nom + " (" + nomCourt + ")"; + } + return nom; + } + + /** Méthode métier pour calculer l'ancienneté en années */ + public int getAncienneteAnnees() { + if (dateFondation == null) { + return 0; + } + return Period.between(dateFondation, LocalDate.now()).getYears(); + } + + /** Méthode métier pour vérifier si l'organisation est récente (moins de 2 ans) */ + public boolean isRecente() { + return getAncienneteAnnees() < 2; + } + + /** Méthode métier pour vérifier si l'organisation est active */ + public boolean isActive() { + return "ACTIVE".equals(statut) && Boolean.TRUE.equals(getActif()); + } + + /** Méthode métier pour ajouter un membre */ + public void ajouterMembre() { + if (nombreMembres == null) { + nombreMembres = 0; + } + nombreMembres++; + } + + /** Méthode métier pour retirer un membre */ + public void retirerMembre() { + if (nombreMembres != null && nombreMembres > 0) { + nombreMembres--; + } + } + + /** Méthode métier pour activer l'organisation */ + public void activer(String utilisateur) { + this.statut = "ACTIVE"; + this.setActif(true); + marquerCommeModifie(utilisateur); + } + + /** Méthode métier pour suspendre l'organisation */ + public void suspendre(String utilisateur) { + this.statut = "SUSPENDUE"; + this.accepteNouveauxMembres = false; + marquerCommeModifie(utilisateur); + } + + /** Méthode métier pour dissoudre l'organisation */ + public void dissoudre(String utilisateur) { + this.statut = "DISSOUTE"; + this.setActif(false); + this.accepteNouveauxMembres = false; + marquerCommeModifie(utilisateur); + } + + /** Marque l'entité comme modifiée */ + public void marquerCommeModifie(String utilisateur) { + this.setDateModification(LocalDateTime.now()); + this.setModifiePar(utilisateur); + if (this.getVersion() != null) { + this.setVersion(this.getVersion() + 1); + } else { + this.setVersion(1L); + } + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); // Appelle le onCreate de BaseEntity + if (statut == null) { + statut = "ACTIVE"; + } + if (typeOrganisation == null) { + typeOrganisation = "ASSOCIATION"; + } + if (devise == null) { + devise = "XOF"; + } + if (niveauHierarchique == null) { + niveauHierarchique = 0; + } + if (nombreMembres == null) { + nombreMembres = 0; + } + if (nombreAdministrateurs == null) { + nombreAdministrateurs = 0; + } + if (organisationPublique == null) { + organisationPublique = true; + } + if (accepteNouveauxMembres == null) { + accepteNouveauxMembres = true; + } + if (cotisationObligatoire == null) { + cotisationObligatoire = false; + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/entity/Paiement.java b/src/main/java/dev/lions/unionflow/server/entity/Paiement.java new file mode 100644 index 0000000..ff583be --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Paiement.java @@ -0,0 +1,169 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement; +import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Paiement centralisée pour tous les types de paiements + * Réutilisable pour cotisations, adhésions, événements, aides + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "paiements", + indexes = { + @Index(name = "idx_paiement_numero_reference", columnList = "numero_reference", unique = true), + @Index(name = "idx_paiement_membre", columnList = "membre_id"), + @Index(name = "idx_paiement_statut", columnList = "statut_paiement"), + @Index(name = "idx_paiement_methode", columnList = "methode_paiement"), + @Index(name = "idx_paiement_date", columnList = "date_paiement") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Paiement extends BaseEntity { + + /** Numéro de référence unique */ + @NotBlank + @Column(name = "numero_reference", unique = true, nullable = false, length = 50) + private String numeroReference; + + /** Montant du paiement */ + @NotNull + @DecimalMin(value = "0.0", message = "Le montant doit être positif") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant", nullable = false, precision = 14, scale = 2) + private BigDecimal montant; + + /** Code devise (ISO 3 lettres) */ + @NotBlank + @Pattern(regexp = "^[A-Z]{3}$", message = "Le code devise doit être un code ISO à 3 lettres") + @Column(name = "code_devise", nullable = false, length = 3) + private String codeDevise; + + /** Méthode de paiement */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "methode_paiement", nullable = false, length = 50) + private MethodePaiement methodePaiement; + + /** Statut du paiement */ + @NotNull + @Enumerated(EnumType.STRING) + @Builder.Default + @Column(name = "statut_paiement", nullable = false, length = 30) + private StatutPaiement statutPaiement = StatutPaiement.EN_ATTENTE; + + /** Date de paiement */ + @Column(name = "date_paiement") + private LocalDateTime datePaiement; + + /** Date de validation */ + @Column(name = "date_validation") + private LocalDateTime dateValidation; + + /** Validateur (email de l'administrateur) */ + @Column(name = "validateur", length = 255) + private String validateur; + + /** Référence externe (numéro transaction, URL preuve, etc.) */ + @Column(name = "reference_externe", length = 500) + private String referenceExterne; + + /** URL de preuve de paiement */ + @Column(name = "url_preuve", length = 1000) + private String urlPreuve; + + /** Commentaires et notes */ + @Column(name = "commentaire", length = 1000) + private String commentaire; + + /** Adresse IP de l'initiateur */ + @Column(name = "ip_address", length = 45) + private String ipAddress; + + /** User-Agent de l'initiateur */ + @Column(name = "user_agent", length = 500) + private String userAgent; + + /** Membre payeur */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id", nullable = false) + private Membre membre; + + /** Relations avec les tables de liaison */ + @OneToMany(mappedBy = "paiement", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List paiementsCotisation = new ArrayList<>(); + + @OneToMany(mappedBy = "paiement", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List paiementsAdhesion = new ArrayList<>(); + + @OneToMany(mappedBy = "paiement", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List paiementsEvenement = new ArrayList<>(); + + @OneToMany(mappedBy = "paiement", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List paiementsAide = new ArrayList<>(); + + /** Relation avec TransactionWave (optionnelle) */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "transaction_wave_id") + private TransactionWave transactionWave; + + /** Méthode métier pour générer un numéro de référence unique */ + public static String genererNumeroReference() { + return "PAY-" + + LocalDateTime.now().getYear() + + "-" + + String.format("%012d", System.currentTimeMillis() % 1000000000000L); + } + + /** Méthode métier pour vérifier si le paiement est validé */ + public boolean isValide() { + return StatutPaiement.VALIDE.equals(statutPaiement); + } + + /** Méthode métier pour vérifier si le paiement peut être modifié */ + public boolean peutEtreModifie() { + return !statutPaiement.isFinalise(); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (numeroReference == null || numeroReference.isEmpty()) { + numeroReference = genererNumeroReference(); + } + if (codeDevise == null || codeDevise.isEmpty()) { + codeDevise = "XOF"; + } + if (statutPaiement == null) { + statutPaiement = StatutPaiement.EN_ATTENTE; + } + if (datePaiement == null) { + datePaiement = LocalDateTime.now(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/PaiementAdhesion.java b/src/main/java/dev/lions/unionflow/server/entity/PaiementAdhesion.java new file mode 100644 index 0000000..628999b --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/PaiementAdhesion.java @@ -0,0 +1,75 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Table de liaison entre Paiement et Adhesion + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "paiements_adhesions", + indexes = { + @Index(name = "idx_paiement_adhesion_paiement", columnList = "paiement_id"), + @Index(name = "idx_paiement_adhesion_adhesion", columnList = "adhesion_id") + }, + uniqueConstraints = { + @UniqueConstraint( + name = "uk_paiement_adhesion", + columnNames = {"paiement_id", "adhesion_id"}) + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class PaiementAdhesion extends BaseEntity { + + /** Paiement */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "paiement_id", nullable = false) + private Paiement paiement; + + /** Adhésion */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "adhesion_id", nullable = false) + private Adhesion adhesion; + + /** Montant appliqué à cette adhésion */ + @NotNull + @DecimalMin(value = "0.0", message = "Le montant appliqué doit être positif") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_applique", nullable = false, precision = 14, scale = 2) + private BigDecimal montantApplique; + + /** Date d'application */ + @Column(name = "date_application") + private LocalDateTime dateApplication; + + /** Commentaire sur l'application */ + @Column(name = "commentaire", length = 500) + private String commentaire; + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (dateApplication == null) { + dateApplication = LocalDateTime.now(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/PaiementAide.java b/src/main/java/dev/lions/unionflow/server/entity/PaiementAide.java new file mode 100644 index 0000000..4f9603f --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/PaiementAide.java @@ -0,0 +1,75 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Table de liaison entre Paiement et DemandeAide + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "paiements_aides", + indexes = { + @Index(name = "idx_paiement_aide_paiement", columnList = "paiement_id"), + @Index(name = "idx_paiement_aide_demande", columnList = "demande_aide_id") + }, + uniqueConstraints = { + @UniqueConstraint( + name = "uk_paiement_aide", + columnNames = {"paiement_id", "demande_aide_id"}) + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class PaiementAide extends BaseEntity { + + /** Paiement */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "paiement_id", nullable = false) + private Paiement paiement; + + /** Demande d'aide */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "demande_aide_id", nullable = false) + private DemandeAide demandeAide; + + /** Montant appliqué à cette demande d'aide */ + @NotNull + @DecimalMin(value = "0.0", message = "Le montant appliqué doit être positif") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_applique", nullable = false, precision = 14, scale = 2) + private BigDecimal montantApplique; + + /** Date d'application */ + @Column(name = "date_application") + private LocalDateTime dateApplication; + + /** Commentaire sur l'application */ + @Column(name = "commentaire", length = 500) + private String commentaire; + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (dateApplication == null) { + dateApplication = LocalDateTime.now(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/PaiementCotisation.java b/src/main/java/dev/lions/unionflow/server/entity/PaiementCotisation.java new file mode 100644 index 0000000..6f4ca60 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/PaiementCotisation.java @@ -0,0 +1,76 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Table de liaison entre Paiement et Cotisation + * Permet à un paiement de couvrir plusieurs cotisations + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "paiements_cotisations", + indexes = { + @Index(name = "idx_paiement_cotisation_paiement", columnList = "paiement_id"), + @Index(name = "idx_paiement_cotisation_cotisation", columnList = "cotisation_id") + }, + uniqueConstraints = { + @UniqueConstraint( + name = "uk_paiement_cotisation", + columnNames = {"paiement_id", "cotisation_id"}) + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class PaiementCotisation extends BaseEntity { + + /** Paiement */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "paiement_id", nullable = false) + private Paiement paiement; + + /** Cotisation */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cotisation_id", nullable = false) + private Cotisation cotisation; + + /** Montant appliqué à cette cotisation */ + @NotNull + @DecimalMin(value = "0.0", message = "Le montant appliqué doit être positif") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_applique", nullable = false, precision = 14, scale = 2) + private BigDecimal montantApplique; + + /** Date d'application */ + @Column(name = "date_application") + private LocalDateTime dateApplication; + + /** Commentaire sur l'application */ + @Column(name = "commentaire", length = 500) + private String commentaire; + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (dateApplication == null) { + dateApplication = LocalDateTime.now(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/PaiementEvenement.java b/src/main/java/dev/lions/unionflow/server/entity/PaiementEvenement.java new file mode 100644 index 0000000..fb0a63b --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/PaiementEvenement.java @@ -0,0 +1,75 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Table de liaison entre Paiement et InscriptionEvenement + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "paiements_evenements", + indexes = { + @Index(name = "idx_paiement_evenement_paiement", columnList = "paiement_id"), + @Index(name = "idx_paiement_evenement_inscription", columnList = "inscription_evenement_id") + }, + uniqueConstraints = { + @UniqueConstraint( + name = "uk_paiement_evenement", + columnNames = {"paiement_id", "inscription_evenement_id"}) + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class PaiementEvenement extends BaseEntity { + + /** Paiement */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "paiement_id", nullable = false) + private Paiement paiement; + + /** Inscription à l'événement */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "inscription_evenement_id", nullable = false) + private InscriptionEvenement inscriptionEvenement; + + /** Montant appliqué à cette inscription */ + @NotNull + @DecimalMin(value = "0.0", message = "Le montant appliqué doit être positif") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_applique", nullable = false, precision = 14, scale = 2) + private BigDecimal montantApplique; + + /** Date d'application */ + @Column(name = "date_application") + private LocalDateTime dateApplication; + + /** Commentaire sur l'application */ + @Column(name = "commentaire", length = 500) + private String commentaire; + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (dateApplication == null) { + dateApplication = LocalDateTime.now(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/Permission.java b/src/main/java/dev/lions/unionflow/server/entity/Permission.java new file mode 100644 index 0000000..8c5bf2c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Permission.java @@ -0,0 +1,90 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Permission pour la gestion des permissions granulaires + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "permissions", + indexes = { + @Index(name = "idx_permission_code", columnList = "code", unique = true), + @Index(name = "idx_permission_module", columnList = "module"), + @Index(name = "idx_permission_ressource", columnList = "ressource") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Permission extends BaseEntity { + + /** Code unique de la permission (format: MODULE > RESSOURCE > ACTION) */ + @NotBlank + @Column(name = "code", unique = true, nullable = false, length = 100) + private String code; + + /** Module (ex: ORGANISATION, MEMBRE, COTISATION) */ + @NotBlank + @Column(name = "module", nullable = false, length = 50) + private String module; + + /** Ressource (ex: MEMBRE, COTISATION, ADHESION) */ + @NotBlank + @Column(name = "ressource", nullable = false, length = 50) + private String ressource; + + /** Action (ex: CREATE, READ, UPDATE, DELETE, VALIDATE) */ + @NotBlank + @Column(name = "action", nullable = false, length = 50) + private String action; + + /** Libellé de la permission */ + @Column(name = "libelle", length = 200) + private String libelle; + + /** Description de la permission */ + @Column(name = "description", length = 500) + private String description; + + /** Rôles associés */ + @OneToMany(mappedBy = "permission", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List roles = new ArrayList<>(); + + /** Méthode métier pour générer le code à partir des composants */ + public static String genererCode(String module, String ressource, String action) { + return String.format("%s > %s > %s", module.toUpperCase(), ressource.toUpperCase(), action.toUpperCase()); + } + + /** Méthode métier pour vérifier si le code est valide */ + public boolean isCodeValide() { + return code != null && code.contains(" > ") && code.split(" > ").length == 3; + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + // Générer le code si non fourni + if (code == null || code.isEmpty()) { + if (module != null && ressource != null && action != null) { + code = genererCode(module, ressource, action); + } + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/PieceJointe.java b/src/main/java/dev/lions/unionflow/server/entity/PieceJointe.java new file mode 100644 index 0000000..6d3155b --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/PieceJointe.java @@ -0,0 +1,103 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité PieceJointe pour l'association flexible de documents + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "pieces_jointes", + indexes = { + @Index(name = "idx_piece_jointe_document", columnList = "document_id"), + @Index(name = "idx_piece_jointe_membre", columnList = "membre_id"), + @Index(name = "idx_piece_jointe_organisation", columnList = "organisation_id"), + @Index(name = "idx_piece_jointe_cotisation", columnList = "cotisation_id"), + @Index(name = "idx_piece_jointe_adhesion", columnList = "adhesion_id"), + @Index(name = "idx_piece_jointe_demande_aide", columnList = "demande_aide_id"), + @Index(name = "idx_piece_jointe_transaction_wave", columnList = "transaction_wave_id") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class PieceJointe extends BaseEntity { + + /** Ordre d'affichage */ + @NotNull + @Min(value = 1, message = "L'ordre doit être positif") + @Column(name = "ordre", nullable = false) + private Integer ordre; + + /** Libellé de la pièce jointe */ + @Column(name = "libelle", length = 200) + private String libelle; + + /** Commentaire */ + @Column(name = "commentaire", length = 500) + private String commentaire; + + /** Document associé */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "document_id", nullable = false) + private Document document; + + // Relations flexibles (une seule doit être renseignée) + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "membre_id") + private Membre membre; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id") + private Organisation organisation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cotisation_id") + private Cotisation cotisation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "adhesion_id") + private Adhesion adhesion; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "demande_aide_id") + private DemandeAide demandeAide; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "transaction_wave_id") + private TransactionWave transactionWave; + + /** Méthode métier pour vérifier qu'une seule relation est renseignée */ + public boolean isValide() { + int count = 0; + if (membre != null) count++; + if (organisation != null) count++; + if (cotisation != null) count++; + if (adhesion != null) count++; + if (demandeAide != null) count++; + if (transactionWave != null) count++; + return count == 1; // Exactement une relation doit être renseignée + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (ordre == null) { + ordre = 1; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/Role.java b/src/main/java/dev/lions/unionflow/server/entity/Role.java new file mode 100644 index 0000000..7ddc3ab --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/Role.java @@ -0,0 +1,105 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité Role pour la gestion des rôles dans le système + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "roles", + indexes = { + @Index(name = "idx_role_code", columnList = "code", unique = true), + @Index(name = "idx_role_actif", columnList = "actif"), + @Index(name = "idx_role_niveau", columnList = "niveau_hierarchique") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class Role extends BaseEntity { + + /** Code unique du rôle */ + @NotBlank + @Column(name = "code", unique = true, nullable = false, length = 50) + private String code; + + /** Libellé du rôle */ + @NotBlank + @Column(name = "libelle", nullable = false, length = 100) + private String libelle; + + /** Description du rôle */ + @Column(name = "description", length = 500) + private String description; + + /** Niveau hiérarchique (plus bas = plus prioritaire) */ + @NotNull + @Builder.Default + @Column(name = "niveau_hierarchique", nullable = false) + private Integer niveauHierarchique = 100; + + /** Type de rôle */ + @Enumerated(EnumType.STRING) + @Column(name = "type_role", nullable = false, length = 50) + private TypeRole typeRole; + + /** Organisation propriétaire (null pour rôles système) */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "organisation_id") + private Organisation organisation; + + /** Permissions associées */ + @OneToMany(mappedBy = "role", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List permissions = new ArrayList<>(); + + /** Énumération des types de rôle */ + public enum TypeRole { + SYSTEME("Rôle Système"), + ORGANISATION("Rôle Organisation"), + PERSONNALISE("Rôle Personnalisé"); + + private final String libelle; + + TypeRole(String libelle) { + this.libelle = libelle; + } + + public String getLibelle() { + return libelle; + } + } + + /** Méthode métier pour vérifier si c'est un rôle système */ + public boolean isRoleSysteme() { + return TypeRole.SYSTEME.equals(typeRole); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (typeRole == null) { + typeRole = TypeRole.PERSONNALISE; + } + if (niveauHierarchique == null) { + niveauHierarchique = 100; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/RolePermission.java b/src/main/java/dev/lions/unionflow/server/entity/RolePermission.java new file mode 100644 index 0000000..6f07add --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/RolePermission.java @@ -0,0 +1,54 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Table de liaison entre Role et Permission + * Permet à un rôle d'avoir plusieurs permissions + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "roles_permissions", + indexes = { + @Index(name = "idx_role_permission_role", columnList = "role_id"), + @Index(name = "idx_role_permission_permission", columnList = "permission_id") + }, + uniqueConstraints = { + @UniqueConstraint( + name = "uk_role_permission", + columnNames = {"role_id", "permission_id"}) + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class RolePermission extends BaseEntity { + + /** Rôle */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "role_id", nullable = false) + private Role role; + + /** Permission */ + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "permission_id", nullable = false) + private Permission permission; + + /** Commentaire sur l'association */ + @Column(name = "commentaire", length = 500) + private String commentaire; +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/TemplateNotification.java b/src/main/java/dev/lions/unionflow/server/entity/TemplateNotification.java new file mode 100644 index 0000000..5adac3a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/TemplateNotification.java @@ -0,0 +1,81 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité TemplateNotification pour les templates de notifications réutilisables + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "templates_notifications", + indexes = { + @Index(name = "idx_template_code", columnList = "code", unique = true), + @Index(name = "idx_template_actif", columnList = "actif") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class TemplateNotification extends BaseEntity { + + /** Code unique du template */ + @NotBlank + @Column(name = "code", unique = true, nullable = false, length = 100) + private String code; + + /** Sujet du template */ + @Column(name = "sujet", length = 500) + private String sujet; + + /** Corps du template (texte) */ + @Column(name = "corps_texte", columnDefinition = "TEXT") + private String corpsTexte; + + /** Corps du template (HTML) */ + @Column(name = "corps_html", columnDefinition = "TEXT") + private String corpsHtml; + + /** Variables disponibles (JSON) */ + @Column(name = "variables_disponibles", columnDefinition = "TEXT") + private String variablesDisponibles; + + /** Canaux supportés (JSON array) */ + @Column(name = "canaux_supportes", length = 500) + private String canauxSupportes; + + /** Langue du template */ + @Column(name = "langue", length = 10) + private String langue; + + /** Description */ + @Column(name = "description", length = 1000) + private String description; + + /** Notifications utilisant ce template */ + @OneToMany(mappedBy = "template", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List notifications = new ArrayList<>(); + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (langue == null || langue.isEmpty()) { + langue = "fr"; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/TransactionWave.java b/src/main/java/dev/lions/unionflow/server/entity/TransactionWave.java new file mode 100644 index 0000000..0c7573f --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/TransactionWave.java @@ -0,0 +1,161 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave; +import dev.lions.unionflow.server.api.enums.wave.TypeTransactionWave; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité TransactionWave pour le suivi des transactions Wave Mobile Money + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "transactions_wave", + indexes = { + @Index(name = "idx_transaction_wave_id", columnList = "wave_transaction_id", unique = true), + @Index(name = "idx_transaction_wave_request_id", columnList = "wave_request_id"), + @Index(name = "idx_transaction_wave_reference", columnList = "wave_reference"), + @Index(name = "idx_transaction_wave_statut", columnList = "statut_transaction"), + @Index(name = "idx_transaction_wave_compte", columnList = "compte_wave_id") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class TransactionWave extends BaseEntity { + + /** Identifiant Wave de la transaction (unique) */ + @NotBlank + @Column(name = "wave_transaction_id", unique = true, nullable = false, length = 100) + private String waveTransactionId; + + /** Identifiant de requête Wave */ + @Column(name = "wave_request_id", length = 100) + private String waveRequestId; + + /** Référence Wave */ + @Column(name = "wave_reference", length = 100) + private String waveReference; + + /** Type de transaction */ + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "type_transaction", nullable = false, length = 50) + private TypeTransactionWave typeTransaction; + + /** Statut de la transaction */ + @NotNull + @Enumerated(EnumType.STRING) + @Builder.Default + @Column(name = "statut_transaction", nullable = false, length = 30) + private StatutTransactionWave statutTransaction = StatutTransactionWave.INITIALISE; + + /** Montant de la transaction */ + @NotNull + @DecimalMin(value = "0.0", message = "Le montant doit être positif") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant", nullable = false, precision = 14, scale = 2) + private BigDecimal montant; + + /** Frais de transaction */ + @DecimalMin(value = "0.0") + @Digits(integer = 10, fraction = 2) + @Column(name = "frais", precision = 12, scale = 2) + private BigDecimal frais; + + /** Montant net (montant - frais) */ + @DecimalMin(value = "0.0") + @Digits(integer = 12, fraction = 2) + @Column(name = "montant_net", precision = 14, scale = 2) + private BigDecimal montantNet; + + /** Code devise */ + @NotBlank + @Pattern(regexp = "^[A-Z]{3}$") + @Column(name = "code_devise", nullable = false, length = 3) + private String codeDevise; + + /** Numéro téléphone payeur */ + @Column(name = "telephone_payeur", length = 13) + private String telephonePayeur; + + /** Numéro téléphone bénéficiaire */ + @Column(name = "telephone_beneficiaire", length = 13) + private String telephoneBeneficiaire; + + /** Métadonnées JSON (réponse complète de Wave API) */ + @Column(name = "metadonnees", columnDefinition = "TEXT") + private String metadonnees; + + /** Réponse complète de Wave API (JSON) */ + @Column(name = "reponse_wave_api", columnDefinition = "TEXT") + private String reponseWaveApi; + + /** Nombre de tentatives */ + @Builder.Default + @Column(name = "nombre_tentatives", nullable = false) + private Integer nombreTentatives = 0; + + /** Date de dernière tentative */ + @Column(name = "date_derniere_tentative") + private LocalDateTime dateDerniereTentative; + + /** Message d'erreur (si échec) */ + @Column(name = "message_erreur", length = 1000) + private String messageErreur; + + // Relations + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "compte_wave_id", nullable = false) + private CompteWave compteWave; + + @OneToMany(mappedBy = "transactionWave", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @Builder.Default + private List webhooks = new ArrayList<>(); + + /** Méthode métier pour vérifier si la transaction est réussie */ + public boolean isReussie() { + return StatutTransactionWave.REUSSIE.equals(statutTransaction); + } + + /** Méthode métier pour vérifier si la transaction peut être retentée */ + public boolean peutEtreRetentee() { + return (statutTransaction == StatutTransactionWave.ECHOUE + || statutTransaction == StatutTransactionWave.EXPIRED) + && (nombreTentatives == null || nombreTentatives < 5); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (statutTransaction == null) { + statutTransaction = StatutTransactionWave.INITIALISE; + } + if (codeDevise == null || codeDevise.isEmpty()) { + codeDevise = "XOF"; + } + if (nombreTentatives == null) { + nombreTentatives = 0; + } + if (montantNet == null && montant != null && frais != null) { + montantNet = montant.subtract(frais); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/entity/TypeOrganisationEntity.java b/src/main/java/dev/lions/unionflow/server/entity/TypeOrganisationEntity.java new file mode 100644 index 0000000..988a9f4 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/TypeOrganisationEntity.java @@ -0,0 +1,73 @@ +package dev.lions.unionflow.server.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +/** + * Entité persistée représentant un type d'organisation. + * + *

Cette entité permet de gérer dynamiquement le catalogue des types d'organisations + * (codes, libellés, description, ordre d'affichage, activation/désactivation). + * + *

Le champ {@code code} doit rester synchronisé avec l'enum {@link + * dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation} pour les types + * standards fournis par la plateforme. + */ +@Entity +@Table( + name = "uf_type_organisation", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_type_organisation_code", + columnNames = {"code"}) + }) +public class TypeOrganisationEntity extends BaseEntity { + + @Column(name = "code", length = 50, nullable = false, unique = true) + private String code; + + @Column(name = "libelle", length = 150, nullable = false) + private String libelle; + + @Column(name = "description", length = 500) + private String description; + + @Column(name = "ordre_affichage") + private Integer ordreAffichage; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getLibelle() { + return libelle; + } + + public void setLibelle(String libelle) { + this.libelle = libelle; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getOrdreAffichage() { + return ordreAffichage; + } + + public void setOrdreAffichage(Integer ordreAffichage) { + this.ordreAffichage = ordreAffichage; + } +} + + diff --git a/src/main/java/dev/lions/unionflow/server/entity/WebhookWave.java b/src/main/java/dev/lions/unionflow/server/entity/WebhookWave.java new file mode 100644 index 0000000..ec8c3e5 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/entity/WebhookWave.java @@ -0,0 +1,118 @@ +package dev.lions.unionflow.server.entity; + +import dev.lions.unionflow.server.api.enums.wave.StatutWebhook; +import dev.lions.unionflow.server.api.enums.wave.TypeEvenementWebhook; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Entité WebhookWave pour le traitement des événements Wave + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Entity +@Table( + name = "webhooks_wave", + indexes = { + @Index(name = "idx_webhook_wave_event_id", columnList = "wave_event_id", unique = true), + @Index(name = "idx_webhook_wave_statut", columnList = "statut_traitement"), + @Index(name = "idx_webhook_wave_type", columnList = "type_evenement"), + @Index(name = "idx_webhook_wave_transaction", columnList = "transaction_wave_id"), + @Index(name = "idx_webhook_wave_paiement", columnList = "paiement_id") + }) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(callSuper = true) +public class WebhookWave extends BaseEntity { + + /** Identifiant unique de l'événement Wave */ + @NotBlank + @Column(name = "wave_event_id", unique = true, nullable = false, length = 100) + private String waveEventId; + + /** Type d'événement */ + @Enumerated(EnumType.STRING) + @Column(name = "type_evenement", length = 50) + private TypeEvenementWebhook typeEvenement; + + /** Statut de traitement */ + @Enumerated(EnumType.STRING) + @Builder.Default + @Column(name = "statut_traitement", nullable = false, length = 30) + private StatutWebhook statutTraitement = StatutWebhook.EN_ATTENTE; + + /** Payload JSON reçu */ + @Column(name = "payload", columnDefinition = "TEXT") + private String payload; + + /** Signature de validation */ + @Column(name = "signature", length = 500) + private String signature; + + /** Date de réception */ + @Column(name = "date_reception") + private LocalDateTime dateReception; + + /** Date de traitement */ + @Column(name = "date_traitement") + private LocalDateTime dateTraitement; + + /** Nombre de tentatives de traitement */ + @Builder.Default + @Column(name = "nombre_tentatives", nullable = false) + private Integer nombreTentatives = 0; + + /** Message d'erreur (si échec) */ + @Column(name = "message_erreur", length = 1000) + private String messageErreur; + + /** Commentaires */ + @Column(name = "commentaire", length = 500) + private String commentaire; + + // Relations + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "transaction_wave_id") + private TransactionWave transactionWave; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "paiement_id") + private Paiement paiement; + + /** Méthode métier pour vérifier si le webhook est traité */ + public boolean isTraite() { + return StatutWebhook.TRAITE.equals(statutTraitement); + } + + /** Méthode métier pour vérifier si le webhook peut être retenté */ + public boolean peutEtreRetente() { + return (statutTraitement == StatutWebhook.ECHOUE || statutTraitement == StatutWebhook.EN_ATTENTE) + && (nombreTentatives == null || nombreTentatives < 5); + } + + /** Callback JPA avant la persistance */ + @PrePersist + protected void onCreate() { + super.onCreate(); + if (statutTraitement == null) { + statutTraitement = StatutWebhook.EN_ATTENTE; + } + if (dateReception == null) { + dateReception = LocalDateTime.now(); + } + if (nombreTentatives == null) { + nombreTentatives = 0; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/exception/JsonProcessingExceptionMapper.java b/src/main/java/dev/lions/unionflow/server/exception/JsonProcessingExceptionMapper.java new file mode 100644 index 0000000..fa9b18e --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/exception/JsonProcessingExceptionMapper.java @@ -0,0 +1,39 @@ +package dev.lions.unionflow.server.exception; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import java.util.Map; +import org.jboss.logging.Logger; + +/** + * Exception mapper pour gérer les erreurs de désérialisation JSON + * Retourne 400 (Bad Request) au lieu de 500 (Internal Server Error) + */ +@Provider +public class JsonProcessingExceptionMapper implements ExceptionMapper { + + private static final Logger LOG = Logger.getLogger(JsonProcessingExceptionMapper.class); + + @Override + public Response toResponse(com.fasterxml.jackson.core.JsonProcessingException exception) { + LOG.warnf("Erreur de désérialisation JSON: %s", exception.getMessage()); + + String message = "Erreur de format JSON"; + if (exception instanceof MismatchedInputException) { + message = "Format JSON invalide ou body manquant"; + } else if (exception instanceof InvalidFormatException) { + message = "Format de données invalide dans le JSON"; + } else if (exception instanceof JsonMappingException) { + message = "Erreur de mapping JSON: " + exception.getMessage(); + } + + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", message, "details", exception.getMessage())) + .build(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/AdhesionRepository.java b/src/main/java/dev/lions/unionflow/server/repository/AdhesionRepository.java new file mode 100644 index 0000000..0929487 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/AdhesionRepository.java @@ -0,0 +1,102 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.Adhesion; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Adhesion + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@ApplicationScoped +public class AdhesionRepository extends BaseRepository { + + public AdhesionRepository() { + super(Adhesion.class); + } + + /** + * Trouve une adhésion par son numéro de référence + * + * @param numeroReference numéro de référence unique + * @return Optional contenant l'adhésion si trouvée + */ + public Optional findByNumeroReference(String numeroReference) { + TypedQuery query = + entityManager.createQuery( + "SELECT a FROM Adhesion a WHERE a.numeroReference = :numeroReference", Adhesion.class); + query.setParameter("numeroReference", numeroReference); + return query.getResultStream().findFirst(); + } + + /** + * Trouve toutes les adhésions d'un membre + * + * @param membreId identifiant du membre + * @return liste des adhésions du membre + */ + public List findByMembreId(UUID membreId) { + TypedQuery query = + entityManager.createQuery( + "SELECT a FROM Adhesion a WHERE a.membre.id = :membreId", Adhesion.class); + query.setParameter("membreId", membreId); + return query.getResultList(); + } + + /** + * Trouve toutes les adhésions d'une organisation + * + * @param organisationId identifiant de l'organisation + * @return liste des adhésions de l'organisation + */ + public List findByOrganisationId(UUID organisationId) { + TypedQuery query = + entityManager.createQuery( + "SELECT a FROM Adhesion a WHERE a.organisation.id = :organisationId", Adhesion.class); + query.setParameter("organisationId", organisationId); + return query.getResultList(); + } + + /** + * Trouve toutes les adhésions par statut + * + * @param statut statut de l'adhésion + * @return liste des adhésions avec le statut spécifié + */ + public List findByStatut(String statut) { + TypedQuery query = + entityManager.createQuery("SELECT a FROM Adhesion a WHERE a.statut = :statut", Adhesion.class); + query.setParameter("statut", statut); + return query.getResultList(); + } + + /** + * Trouve toutes les adhésions en attente + * + * @return liste des adhésions en attente + */ + public List findEnAttente() { + return findByStatut("EN_ATTENTE"); + } + + /** + * Trouve toutes les adhésions approuvées en attente de paiement + * + * @return liste des adhésions approuvées non payées + */ + public List findApprouveesEnAttentePaiement() { + TypedQuery query = + entityManager.createQuery( + "SELECT a FROM Adhesion a WHERE a.statut = :statut AND (a.montantPaye IS NULL OR a.montantPaye < a.fraisAdhesion)", + Adhesion.class); + query.setParameter("statut", "APPROUVEE"); + return query.getResultList(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/AdresseRepository.java b/src/main/java/dev/lions/unionflow/server/repository/AdresseRepository.java new file mode 100644 index 0000000..57aee11 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/AdresseRepository.java @@ -0,0 +1,111 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.adresse.TypeAdresse; +import dev.lions.unionflow.server.entity.Adresse; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Adresse + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class AdresseRepository implements PanacheRepository { + + /** + * Trouve une adresse par son UUID + * + * @param id UUID de l'adresse + * @return Adresse ou Optional.empty() + */ + public Optional findAdresseById(UUID id) { + return find("id = ?1", id).firstResultOptional(); + } + + /** + * Trouve toutes les adresses d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des adresses + */ + public List findByOrganisationId(UUID organisationId) { + return find("organisation.id", organisationId).list(); + } + + /** + * Trouve l'adresse principale d'une organisation + * + * @param organisationId ID de l'organisation + * @return Adresse principale ou Optional.empty() + */ + public Optional findPrincipaleByOrganisationId(UUID organisationId) { + return find("organisation.id = ?1 AND principale = true", organisationId).firstResultOptional(); + } + + /** + * Trouve toutes les adresses d'un membre + * + * @param membreId ID du membre + * @return Liste des adresses + */ + public List findByMembreId(UUID membreId) { + return find("membre.id", membreId).list(); + } + + /** + * Trouve l'adresse principale d'un membre + * + * @param membreId ID du membre + * @return Adresse principale ou Optional.empty() + */ + public Optional findPrincipaleByMembreId(UUID membreId) { + return find("membre.id = ?1 AND principale = true", membreId).firstResultOptional(); + } + + /** + * Trouve l'adresse d'un événement + * + * @param evenementId ID de l'événement + * @return Adresse ou Optional.empty() + */ + public Optional findByEvenementId(UUID evenementId) { + return find("evenement.id", evenementId).firstResultOptional(); + } + + /** + * Trouve les adresses par type + * + * @param typeAdresse Type d'adresse + * @return Liste des adresses + */ + public List findByType(TypeAdresse typeAdresse) { + return find("typeAdresse", typeAdresse).list(); + } + + /** + * Trouve les adresses par ville + * + * @param ville Nom de la ville + * @return Liste des adresses + */ + public List findByVille(String ville) { + return find("LOWER(ville) = LOWER(?1)", ville).list(); + } + + /** + * Trouve les adresses par pays + * + * @param pays Nom du pays + * @return Liste des adresses + */ + public List findByPays(String pays) { + return find("LOWER(pays) = LOWER(?1)", pays).list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/AuditLogRepository.java b/src/main/java/dev/lions/unionflow/server/repository/AuditLogRepository.java new file mode 100644 index 0000000..bd78702 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/AuditLogRepository.java @@ -0,0 +1,26 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.AuditLog; +import jakarta.enterprise.context.ApplicationScoped; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +/** + * Repository pour les logs d'audit + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@ApplicationScoped +public class AuditLogRepository extends BaseRepository { + + public AuditLogRepository() { + super(AuditLog.class); + } + + // Les méthodes de recherche spécifiques peuvent être ajoutées ici si nécessaire + // Pour l'instant, on utilise les méthodes de base et les requêtes dans le service +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/BaseRepository.java b/src/main/java/dev/lions/unionflow/server/repository/BaseRepository.java new file mode 100644 index 0000000..de2db0a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/BaseRepository.java @@ -0,0 +1,148 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.BaseEntity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository de base pour les entités utilisant UUID comme identifiant + * + *

Remplace PanacheRepository pour utiliser UUID au lieu de Long. + * Fournit les fonctionnalités de base de Panache avec UUID. + * + * @param Le type d'entité qui étend BaseEntity + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +public abstract class BaseRepository { + + @PersistenceContext + protected EntityManager entityManager; + + protected final Class entityClass; + + protected BaseRepository(Class entityClass) { + this.entityClass = entityClass; + } + + /** + * Trouve une entité par son UUID + * + * @param id L'UUID de l'entité + * @return L'entité trouvée ou null + */ + public T findById(UUID id) { + return entityManager.find(entityClass, id); + } + + /** + * Trouve une entité par son UUID (retourne Optional) + * + * @param id L'UUID de l'entité + * @return Optional contenant l'entité si trouvée + */ + public Optional findByIdOptional(UUID id) { + return Optional.ofNullable(findById(id)); + } + + /** + * Persiste une entité + * + * @param entity L'entité à persister + */ + @Transactional + public void persist(T entity) { + entityManager.persist(entity); + } + + /** + * Met à jour une entité + * + * @param entity L'entité à mettre à jour + * @return L'entité mise à jour + */ + @Transactional + public T update(T entity) { + return entityManager.merge(entity); + } + + /** + * Supprime une entité + * + * @param entity L'entité à supprimer + */ + @Transactional + public void delete(T entity) { + // Si l'entité n'est pas dans le contexte de persistance, la merger d'abord + if (!entityManager.contains(entity)) { + entity = entityManager.merge(entity); + } + entityManager.remove(entity); + } + + /** + * Supprime une entité par son UUID + * + * @param id L'UUID de l'entité à supprimer + */ + @Transactional + public boolean deleteById(UUID id) { + T entity = findById(id); + if (entity != null) { + // S'assurer que l'entité est dans le contexte de persistance + if (!entityManager.contains(entity)) { + entity = entityManager.merge(entity); + } + entityManager.remove(entity); + return true; + } + return false; + } + + /** + * Liste toutes les entités + * + * @return La liste de toutes les entités + */ + public List listAll() { + return entityManager.createQuery( + "SELECT e FROM " + entityClass.getSimpleName() + " e", entityClass) + .getResultList(); + } + + /** + * Compte toutes les entités + * + * @return Le nombre total d'entités + */ + public long count() { + return entityManager.createQuery( + "SELECT COUNT(e) FROM " + entityClass.getSimpleName() + " e", Long.class) + .getSingleResult(); + } + + /** + * Vérifie si une entité existe par son UUID + * + * @param id L'UUID de l'entité + * @return true si l'entité existe + */ + public boolean existsById(UUID id) { + return findById(id) != null; + } + + /** + * Obtient l'EntityManager (pour les requêtes avancées) + * + * @return L'EntityManager + */ + public EntityManager getEntityManager() { + return entityManager; + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/CompteComptableRepository.java b/src/main/java/dev/lions/unionflow/server/repository/CompteComptableRepository.java new file mode 100644 index 0000000..99e3851 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/CompteComptableRepository.java @@ -0,0 +1,80 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.comptabilite.TypeCompteComptable; +import dev.lions.unionflow.server.entity.CompteComptable; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité CompteComptable + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class CompteComptableRepository implements PanacheRepository { + + /** + * Trouve un compte comptable par son UUID + * + * @param id UUID du compte comptable + * @return Compte comptable ou Optional.empty() + */ + public Optional findCompteComptableById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve un compte par son numéro + * + * @param numeroCompte Numéro du compte + * @return Compte ou Optional.empty() + */ + public Optional findByNumeroCompte(String numeroCompte) { + return find("numeroCompte = ?1 AND actif = true", numeroCompte).firstResultOptional(); + } + + /** + * Trouve les comptes par type + * + * @param type Type de compte + * @return Liste des comptes + */ + public List findByType(TypeCompteComptable type) { + return find("typeCompte = ?1 AND actif = true ORDER BY numeroCompte ASC", type).list(); + } + + /** + * Trouve les comptes par classe comptable + * + * @param classe Classe comptable (1-7) + * @return Liste des comptes + */ + public List findByClasse(Integer classe) { + return find("classeComptable = ?1 AND actif = true ORDER BY numeroCompte ASC", classe).list(); + } + + /** + * Trouve tous les comptes actifs + * + * @return Liste des comptes actifs + */ + public List findAllActifs() { + return find("actif = true ORDER BY numeroCompte ASC").list(); + } + + /** + * Trouve les comptes de trésorerie + * + * @return Liste des comptes de trésorerie + */ + public List findComptesTresorerie() { + return find("typeCompte = ?1 AND actif = true ORDER BY numeroCompte ASC", TypeCompteComptable.TRESORERIE) + .list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/CompteWaveRepository.java b/src/main/java/dev/lions/unionflow/server/repository/CompteWaveRepository.java new file mode 100644 index 0000000..7a63944 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/CompteWaveRepository.java @@ -0,0 +1,98 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.wave.StatutCompteWave; +import dev.lions.unionflow.server.entity.CompteWave; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité CompteWave + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class CompteWaveRepository implements PanacheRepository { + + /** + * Trouve un compte Wave par son UUID + * + * @param id UUID du compte Wave + * @return Compte Wave ou Optional.empty() + */ + public Optional findCompteWaveById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve un compte Wave par numéro de téléphone + * + * @param numeroTelephone Numéro de téléphone + * @return Compte Wave ou Optional.empty() + */ + public Optional findByNumeroTelephone(String numeroTelephone) { + return find("numeroTelephone = ?1 AND actif = true", numeroTelephone).firstResultOptional(); + } + + /** + * Trouve tous les comptes Wave d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des comptes Wave + */ + public List findByOrganisationId(UUID organisationId) { + return find("organisation.id = ?1 AND actif = true", organisationId).list(); + } + + /** + * Trouve le compte Wave principal d'une organisation (premier vérifié) + * + * @param organisationId ID de l'organisation + * @return Compte Wave ou Optional.empty() + */ + public Optional findPrincipalByOrganisationId(UUID organisationId) { + return find( + "organisation.id = ?1 AND statutCompte = ?2 AND actif = true", + organisationId, + StatutCompteWave.VERIFIE) + .firstResultOptional(); + } + + /** + * Trouve tous les comptes Wave d'un membre + * + * @param membreId ID du membre + * @return Liste des comptes Wave + */ + public List findByMembreId(UUID membreId) { + return find("membre.id = ?1 AND actif = true", membreId).list(); + } + + /** + * Trouve le compte Wave principal d'un membre (premier vérifié) + * + * @param membreId ID du membre + * @return Compte Wave ou Optional.empty() + */ + public Optional findPrincipalByMembreId(UUID membreId) { + return find( + "membre.id = ?1 AND statutCompte = ?2 AND actif = true", + membreId, + StatutCompteWave.VERIFIE) + .firstResultOptional(); + } + + /** + * Trouve tous les comptes Wave vérifiés + * + * @return Liste des comptes Wave vérifiés + */ + public List findComptesVerifies() { + return find("statutCompte = ?1 AND actif = true", StatutCompteWave.VERIFIE).list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/ConfigurationWaveRepository.java b/src/main/java/dev/lions/unionflow/server/repository/ConfigurationWaveRepository.java new file mode 100644 index 0000000..0b8452a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/ConfigurationWaveRepository.java @@ -0,0 +1,59 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.ConfigurationWave; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité ConfigurationWave + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class ConfigurationWaveRepository implements PanacheRepository { + + /** + * Trouve une configuration Wave par son UUID + * + * @param id UUID de la configuration + * @return Configuration ou Optional.empty() + */ + public Optional findConfigurationWaveById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve une configuration par sa clé + * + * @param cle Clé de configuration + * @return Configuration ou Optional.empty() + */ + public Optional findByCle(String cle) { + return find("cle = ?1 AND actif = true", cle).firstResultOptional(); + } + + /** + * Trouve toutes les configurations d'un environnement + * + * @param environnement Environnement (SANDBOX, PRODUCTION, COMMON) + * @return Liste des configurations + */ + public List findByEnvironnement(String environnement) { + return find("environnement = ?1 AND actif = true", environnement).list(); + } + + /** + * Trouve toutes les configurations actives + * + * @return Liste des configurations actives + */ + public List findAllActives() { + return find("actif = true ORDER BY cle ASC").list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/CotisationRepository.java b/src/main/java/dev/lions/unionflow/server/repository/CotisationRepository.java new file mode 100644 index 0000000..0c6863c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/CotisationRepository.java @@ -0,0 +1,392 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.Cotisation; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour la gestion des cotisations avec UUID + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +@ApplicationScoped +public class CotisationRepository extends BaseRepository { + + public CotisationRepository() { + super(Cotisation.class); + } + + /** + * Trouve une cotisation par son numéro de référence + * + * @param numeroReference le numéro de référence unique + * @return Optional contenant la cotisation si trouvée + */ + public Optional findByNumeroReference(String numeroReference) { + TypedQuery query = entityManager.createQuery( + "SELECT c FROM Cotisation c WHERE c.numeroReference = :numeroReference", + Cotisation.class); + query.setParameter("numeroReference", numeroReference); + return query.getResultStream().findFirst(); + } + + /** + * Trouve toutes les cotisations d'un membre + * + * @param membreId l'UUID du membre + * @param page pagination + * @param sort tri + * @return liste paginée des cotisations + */ + public List findByMembreId(UUID membreId, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT c FROM Cotisation c WHERE c.membre.id = :membreId" + orderBy, + Cotisation.class); + query.setParameter("membreId", membreId); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les cotisations par statut + * + * @param statut le statut recherché + * @param page pagination + * @return liste paginée des cotisations + */ + public List findByStatut(String statut, Page page) { + TypedQuery query = entityManager.createQuery( + "SELECT c FROM Cotisation c WHERE c.statut = :statut ORDER BY c.dateEcheance DESC", + Cotisation.class); + query.setParameter("statut", statut); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les cotisations en retard + * + * @param dateReference date de référence (généralement aujourd'hui) + * @param page pagination + * @return liste des cotisations en retard + */ + public List findCotisationsEnRetard(LocalDate dateReference, Page page) { + TypedQuery query = entityManager.createQuery( + "SELECT c FROM Cotisation c WHERE c.dateEcheance < :dateReference AND c.statut != 'PAYEE' AND c.statut != 'ANNULEE' ORDER BY c.dateEcheance ASC", + Cotisation.class); + query.setParameter("dateReference", dateReference); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les cotisations par période (année/mois) + * + * @param annee l'année + * @param mois le mois (optionnel) + * @param page pagination + * @return liste des cotisations de la période + */ + public List findByPeriode(Integer annee, Integer mois, Page page) { + TypedQuery query; + if (mois != null) { + query = entityManager.createQuery( + "SELECT c FROM Cotisation c WHERE c.annee = :annee AND c.mois = :mois ORDER BY c.dateEcheance DESC", + Cotisation.class); + query.setParameter("annee", annee); + query.setParameter("mois", mois); + } else { + query = entityManager.createQuery( + "SELECT c FROM Cotisation c WHERE c.annee = :annee ORDER BY c.mois DESC, c.dateEcheance DESC", + Cotisation.class); + query.setParameter("annee", annee); + } + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les cotisations par type + * + * @param typeCotisation le type de cotisation + * @param page pagination + * @return liste des cotisations du type spécifié + */ + public List findByType(String typeCotisation, Page page) { + TypedQuery query = entityManager.createQuery( + "SELECT c FROM Cotisation c WHERE c.typeCotisation = :typeCotisation ORDER BY c.dateEcheance DESC", + Cotisation.class); + query.setParameter("typeCotisation", typeCotisation); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Recherche avancée avec filtres multiples + * + * @param membreId UUID du membre (optionnel) + * @param statut statut (optionnel) + * @param typeCotisation type (optionnel) + * @param annee année (optionnel) + * @param mois mois (optionnel) + * @param page pagination + * @return liste filtrée des cotisations + */ + public List rechercheAvancee( + UUID membreId, String statut, String typeCotisation, Integer annee, Integer mois, Page page) { + StringBuilder jpql = new StringBuilder("SELECT c FROM Cotisation c WHERE 1=1"); + Map params = new HashMap<>(); + + if (membreId != null) { + jpql.append(" AND c.membre.id = :membreId"); + params.put("membreId", membreId); + } + + if (statut != null && !statut.isEmpty()) { + jpql.append(" AND c.statut = :statut"); + params.put("statut", statut); + } + + if (typeCotisation != null && !typeCotisation.isEmpty()) { + jpql.append(" AND c.typeCotisation = :typeCotisation"); + params.put("typeCotisation", typeCotisation); + } + + if (annee != null) { + jpql.append(" AND c.annee = :annee"); + params.put("annee", annee); + } + + if (mois != null) { + jpql.append(" AND c.mois = :mois"); + params.put("mois", mois); + } + + jpql.append(" ORDER BY c.dateEcheance DESC"); + + TypedQuery query = entityManager.createQuery(jpql.toString(), Cotisation.class); + for (Map.Entry param : params.entrySet()) { + query.setParameter(param.getKey(), param.getValue()); + } + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Calcule le total des montants dus pour un membre + * + * @param membreId UUID du membre + * @return montant total dû + */ + public BigDecimal calculerTotalMontantDu(UUID membreId) { + TypedQuery query = entityManager.createQuery( + "SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c WHERE c.membre.id = :membreId", + BigDecimal.class); + query.setParameter("membreId", membreId); + BigDecimal result = query.getSingleResult(); + return result != null ? result : BigDecimal.ZERO; + } + + /** + * Calcule le total des montants payés pour un membre + * + * @param membreId UUID du membre + * @return montant total payé + */ + public BigDecimal calculerTotalMontantPaye(UUID membreId) { + TypedQuery query = entityManager.createQuery( + "SELECT COALESCE(SUM(c.montantPaye), 0) FROM Cotisation c WHERE c.membre.id = :membreId", + BigDecimal.class); + query.setParameter("membreId", membreId); + BigDecimal result = query.getSingleResult(); + return result != null ? result : BigDecimal.ZERO; + } + + /** + * Compte les cotisations par statut + * + * @param statut le statut + * @return nombre de cotisations + */ + public long compterParStatut(String statut) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(c) FROM Cotisation c WHERE c.statut = :statut", Long.class); + query.setParameter("statut", statut); + return query.getSingleResult(); + } + + /** + * Trouve les cotisations nécessitant un rappel + * + * @param joursAvantEcheance nombre de jours avant échéance + * @param nombreMaxRappels nombre maximum de rappels déjà envoyés + * @return liste des cotisations à rappeler + */ + public List findCotisationsAuRappel(int joursAvantEcheance, int nombreMaxRappels) { + LocalDate dateRappel = LocalDate.now().plusDays(joursAvantEcheance); + TypedQuery query = entityManager.createQuery( + "SELECT c FROM Cotisation c WHERE c.dateEcheance <= :dateRappel AND c.statut != 'PAYEE' AND c.statut != 'ANNULEE' AND c.nombreRappels < :nombreMaxRappels ORDER BY c.dateEcheance ASC", + Cotisation.class); + query.setParameter("dateRappel", dateRappel); + query.setParameter("nombreMaxRappels", nombreMaxRappels); + return query.getResultList(); + } + + /** + * Met à jour le nombre de rappels pour une cotisation + * + * @param cotisationId UUID de la cotisation + * @return true si mise à jour réussie + */ + public boolean incrementerNombreRappels(UUID cotisationId) { + Cotisation cotisation = findByIdOptional(cotisationId).orElse(null); + if (cotisation != null) { + cotisation.setNombreRappels( + cotisation.getNombreRappels() != null ? cotisation.getNombreRappels() + 1 : 1); + cotisation.setDateDernierRappel(LocalDateTime.now()); + update(cotisation); + return true; + } + return false; + } + + /** + * Statistiques des cotisations par période + * + * @param annee l'année + * @param mois le mois (optionnel) + * @return map avec les statistiques + */ + public Map getStatistiquesPeriode(Integer annee, Integer mois) { + String baseQuery = mois != null + ? "SELECT c FROM Cotisation c WHERE c.annee = :annee AND c.mois = :mois" + : "SELECT c FROM Cotisation c WHERE c.annee = :annee"; + + TypedQuery countQuery; + TypedQuery montantTotalQuery; + TypedQuery montantPayeQuery; + TypedQuery payeesQuery; + + if (mois != null) { + countQuery = entityManager.createQuery( + "SELECT COUNT(c) FROM Cotisation c WHERE c.annee = :annee AND c.mois = :mois", + Long.class); + montantTotalQuery = entityManager.createQuery( + "SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c WHERE c.annee = :annee AND c.mois = :mois", + BigDecimal.class); + montantPayeQuery = entityManager.createQuery( + "SELECT COALESCE(SUM(c.montantPaye), 0) FROM Cotisation c WHERE c.annee = :annee AND c.mois = :mois", + BigDecimal.class); + payeesQuery = entityManager.createQuery( + "SELECT COUNT(c) FROM Cotisation c WHERE c.annee = :annee AND c.mois = :mois AND c.statut = 'PAYEE'", + Long.class); + + countQuery.setParameter("annee", annee); + countQuery.setParameter("mois", mois); + montantTotalQuery.setParameter("annee", annee); + montantTotalQuery.setParameter("mois", mois); + montantPayeQuery.setParameter("annee", annee); + montantPayeQuery.setParameter("mois", mois); + payeesQuery.setParameter("annee", annee); + payeesQuery.setParameter("mois", mois); + } else { + countQuery = entityManager.createQuery( + "SELECT COUNT(c) FROM Cotisation c WHERE c.annee = :annee", Long.class); + montantTotalQuery = entityManager.createQuery( + "SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c WHERE c.annee = :annee", + BigDecimal.class); + montantPayeQuery = entityManager.createQuery( + "SELECT COALESCE(SUM(c.montantPaye), 0) FROM Cotisation c WHERE c.annee = :annee", + BigDecimal.class); + payeesQuery = entityManager.createQuery( + "SELECT COUNT(c) FROM Cotisation c WHERE c.annee = :annee AND c.statut = 'PAYEE'", + Long.class); + + countQuery.setParameter("annee", annee); + montantTotalQuery.setParameter("annee", annee); + montantPayeQuery.setParameter("annee", annee); + payeesQuery.setParameter("annee", annee); + } + + Long totalCotisations = countQuery.getSingleResult(); + BigDecimal montantTotal = montantTotalQuery.getSingleResult(); + BigDecimal montantPaye = montantPayeQuery.getSingleResult(); + Long cotisationsPayees = payeesQuery.getSingleResult(); + + return Map.of( + "totalCotisations", totalCotisations != null ? totalCotisations : 0L, + "montantTotal", montantTotal != null ? montantTotal : BigDecimal.ZERO, + "montantPaye", montantPaye != null ? montantPaye : BigDecimal.ZERO, + "cotisationsPayees", cotisationsPayees != null ? cotisationsPayees : 0L, + "tauxPaiement", + totalCotisations != null && totalCotisations > 0 + ? (cotisationsPayees != null ? cotisationsPayees : 0L) * 100.0 / totalCotisations + : 0.0); + } + + /** Somme des montants payés dans une période */ + public BigDecimal sumMontantsPayes( + UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT COALESCE(SUM(c.montantPaye), 0) FROM Cotisation c WHERE c.membre.organisation.id = :organisationId AND c.statut = 'PAYEE' AND c.datePaiement BETWEEN :debut AND :fin", + BigDecimal.class); + query.setParameter("organisationId", organisationId); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + BigDecimal result = query.getSingleResult(); + return result != null ? result : BigDecimal.ZERO; + } + + /** Somme des montants en attente dans une période */ + public BigDecimal sumMontantsEnAttente( + UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c WHERE c.membre.organisation.id = :organisationId AND c.statut = 'EN_ATTENTE' AND c.dateCreation BETWEEN :debut AND :fin", + BigDecimal.class); + query.setParameter("organisationId", organisationId); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + BigDecimal result = query.getSingleResult(); + return result != null ? result : BigDecimal.ZERO; + } + + /** Construit la clause ORDER BY à partir d'un Sort */ + private String buildOrderBy(Sort sort) { + if (sort == null || sort.getColumns().isEmpty()) { + return "c.dateEcheance DESC"; + } + StringBuilder orderBy = new StringBuilder(); + for (int i = 0; i < sort.getColumns().size(); i++) { + if (i > 0) { + orderBy.append(", "); + } + Sort.Column column = sort.getColumns().get(i); + orderBy.append("c.").append(column.getName()); + if (column.getDirection() == Sort.Direction.Descending) { + orderBy.append(" DESC"); + } else { + orderBy.append(" ASC"); + } + } + return orderBy.toString(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/DemandeAideRepository.java b/src/main/java/dev/lions/unionflow/server/repository/DemandeAideRepository.java new file mode 100644 index 0000000..d44bf34 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/DemandeAideRepository.java @@ -0,0 +1,275 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import dev.lions.unionflow.server.entity.DemandeAide; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** Repository pour les demandes d'aide avec UUID */ +@ApplicationScoped +public class DemandeAideRepository extends BaseRepository { + + public DemandeAideRepository() { + super(DemandeAide.class); + } + + /** Trouve toutes les demandes d'aide par organisation */ + public List findByOrganisationId(UUID organisationId) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.organisation.id = :organisationId", + DemandeAide.class); + query.setParameter("organisationId", organisationId); + return query.getResultList(); + } + + /** Trouve toutes les demandes d'aide par organisation avec pagination */ + public List findByOrganisationId(UUID organisationId, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : " ORDER BY d.dateDemande DESC"; + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.organisation.id = :organisationId" + orderBy, + DemandeAide.class); + query.setParameter("organisationId", organisationId); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** Trouve toutes les demandes d'aide par demandeur */ + public List findByDemandeurId(UUID demandeurId) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.demandeur.id = :demandeurId", + DemandeAide.class); + query.setParameter("demandeurId", demandeurId); + return query.getResultList(); + } + + /** Trouve toutes les demandes d'aide par statut */ + public List findByStatut(StatutAide statut) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.statut = :statut", + DemandeAide.class); + query.setParameter("statut", statut); + return query.getResultList(); + } + + /** Trouve toutes les demandes d'aide par statut et organisation */ + public List findByStatutAndOrganisationId(StatutAide statut, UUID organisationId) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.statut = :statut AND d.organisation.id = :organisationId", + DemandeAide.class); + query.setParameter("statut", statut); + query.setParameter("organisationId", organisationId); + return query.getResultList(); + } + + /** Trouve toutes les demandes d'aide par type */ + public List findByTypeAide(TypeAide typeAide) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.typeAide = :typeAide", + DemandeAide.class); + query.setParameter("typeAide", typeAide); + return query.getResultList(); + } + + /** Trouve toutes les demandes d'aide urgentes */ + public List findUrgentes() { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.urgence = true", + DemandeAide.class); + return query.getResultList(); + } + + /** Trouve toutes les demandes d'aide urgentes par organisation */ + public List findUrgentesByOrganisationId(UUID organisationId) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.urgence = true AND d.organisation.id = :organisationId", + DemandeAide.class); + query.setParameter("organisationId", organisationId); + return query.getResultList(); + } + + /** Trouve toutes les demandes d'aide dans une période */ + public List findByPeriode(LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.dateDemande >= :debut AND d.dateDemande <= :fin", + DemandeAide.class); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + return query.getResultList(); + } + + /** Trouve toutes les demandes d'aide dans une période pour une organisation */ + public List findByPeriodeAndOrganisationId( + LocalDateTime debut, LocalDateTime fin, UUID organisationId) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.dateDemande >= :debut AND d.dateDemande <= :fin AND d.organisation.id = :organisationId", + DemandeAide.class); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + query.setParameter("organisationId", organisationId); + return query.getResultList(); + } + + /** Compte le nombre de demandes par statut */ + public long countByStatut(StatutAide statut) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(d) FROM DemandeAide d WHERE d.statut = :statut", + Long.class); + query.setParameter("statut", statut); + return query.getSingleResult(); + } + + /** Compte le nombre de demandes par statut et organisation */ + public long countByStatutAndOrganisationId(StatutAide statut, UUID organisationId) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(d) FROM DemandeAide d WHERE d.statut = :statut AND d.organisation.id = :organisationId", + Long.class); + query.setParameter("statut", statut); + query.setParameter("organisationId", organisationId); + return query.getSingleResult(); + } + + /** Calcule le montant total demandé par organisation */ + public Optional sumMontantDemandeByOrganisationId(UUID organisationId) { + TypedQuery query = entityManager.createQuery( + "SELECT COALESCE(SUM(d.montantDemande), 0) FROM DemandeAide d WHERE d.organisation.id = :organisationId", + BigDecimal.class); + query.setParameter("organisationId", organisationId); + BigDecimal result = query.getSingleResult(); + return result != null && result.compareTo(BigDecimal.ZERO) > 0 + ? Optional.of(result) + : Optional.empty(); + } + + /** Calcule le montant total approuvé par organisation */ + public Optional sumMontantApprouveByOrganisationId(UUID organisationId) { + TypedQuery query = entityManager.createQuery( + "SELECT COALESCE(SUM(d.montantApprouve), 0) FROM DemandeAide d WHERE d.organisation.id = :organisationId AND d.statut = :statut", + BigDecimal.class); + query.setParameter("organisationId", organisationId); + query.setParameter("statut", StatutAide.APPROUVEE); + BigDecimal result = query.getSingleResult(); + return result != null && result.compareTo(BigDecimal.ZERO) > 0 + ? Optional.of(result) + : Optional.empty(); + } + + /** Trouve les demandes d'aide récentes (dernières 30 jours) */ + public List findRecentes() { + LocalDateTime il30Jours = LocalDateTime.now().minusDays(30); + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.dateDemande >= :il30Jours ORDER BY d.dateDemande DESC", + DemandeAide.class); + query.setParameter("il30Jours", il30Jours); + return query.getResultList(); + } + + /** Trouve les demandes d'aide récentes par organisation */ + public List findRecentesByOrganisationId(UUID organisationId) { + LocalDateTime il30Jours = LocalDateTime.now().minusDays(30); + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.dateDemande >= :il30Jours AND d.organisation.id = :organisationId ORDER BY d.dateDemande DESC", + DemandeAide.class); + query.setParameter("il30Jours", il30Jours); + query.setParameter("organisationId", organisationId); + return query.getResultList(); + } + + /** Trouve les demandes d'aide en attente depuis plus de X jours */ + public List findEnAttenteDepuis(int nombreJours) { + LocalDateTime dateLimit = LocalDateTime.now().minusDays(nombreJours); + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.statut = :statut AND d.dateDemande <= :dateLimit", + DemandeAide.class); + query.setParameter("statut", StatutAide.EN_ATTENTE); + query.setParameter("dateLimit", dateLimit); + return query.getResultList(); + } + + /** Trouve les demandes d'aide par évaluateur */ + public List findByEvaluateurId(UUID evaluateurId) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.evaluateur.id = :evaluateurId", + DemandeAide.class); + query.setParameter("evaluateurId", evaluateurId); + return query.getResultList(); + } + + /** Trouve les demandes d'aide en cours d'évaluation par évaluateur */ + public List findEnCoursEvaluationByEvaluateurId(UUID evaluateurId) { + TypedQuery query = entityManager.createQuery( + "SELECT d FROM DemandeAide d WHERE d.evaluateur.id = :evaluateurId AND d.statut = :statut", + DemandeAide.class); + query.setParameter("evaluateurId", evaluateurId); + query.setParameter("statut", StatutAide.EN_COURS_EVALUATION); + return query.getResultList(); + } + + /** Compte les demandes approuvées dans une période */ + public long countDemandesApprouvees(UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(d) FROM DemandeAide d WHERE d.organisation.id = :organisationId AND d.statut = :statut AND d.dateCreation BETWEEN :debut AND :fin", + Long.class); + query.setParameter("organisationId", organisationId); + query.setParameter("statut", StatutAide.APPROUVEE); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + return query.getSingleResult(); + } + + /** Compte toutes les demandes dans une période */ + public long countDemandes(UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(d) FROM DemandeAide d WHERE d.organisation.id = :organisationId AND d.dateCreation BETWEEN :debut AND :fin", + Long.class); + query.setParameter("organisationId", organisationId); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + return query.getSingleResult(); + } + + /** Somme des montants accordés dans une période */ + public BigDecimal sumMontantsAccordes( + UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT COALESCE(SUM(d.montantApprouve), 0) FROM DemandeAide d WHERE d.organisation.id = :organisationId AND d.statut = :statut AND d.dateCreation BETWEEN :debut AND :fin", + BigDecimal.class); + query.setParameter("organisationId", organisationId); + query.setParameter("statut", StatutAide.APPROUVEE); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + BigDecimal result = query.getSingleResult(); + return result != null ? result : BigDecimal.ZERO; + } + + /** Construit la clause ORDER BY à partir d'un Sort */ + private String buildOrderBy(Sort sort) { + if (sort == null || sort.getColumns().isEmpty()) { + return "d.dateDemande DESC"; + } + StringBuilder orderBy = new StringBuilder(); + for (int i = 0; i < sort.getColumns().size(); i++) { + if (i > 0) { + orderBy.append(", "); + } + Sort.Column column = sort.getColumns().get(i); + orderBy.append("d.").append(column.getName()); + if (column.getDirection() == Sort.Direction.Descending) { + orderBy.append(" DESC"); + } else { + orderBy.append(" ASC"); + } + } + return orderBy.toString(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/DocumentRepository.java b/src/main/java/dev/lions/unionflow/server/repository/DocumentRepository.java new file mode 100644 index 0000000..d97904a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/DocumentRepository.java @@ -0,0 +1,70 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.document.TypeDocument; +import dev.lions.unionflow.server.entity.Document; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Document + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class DocumentRepository implements PanacheRepository { + + /** + * Trouve un document par son UUID + * + * @param id UUID du document + * @return Document ou Optional.empty() + */ + public Optional findDocumentById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve un document par son hash MD5 + * + * @param hashMd5 Hash MD5 + * @return Document ou Optional.empty() + */ + public Optional findByHashMd5(String hashMd5) { + return find("hashMd5 = ?1 AND actif = true", hashMd5).firstResultOptional(); + } + + /** + * Trouve un document par son hash SHA256 + * + * @param hashSha256 Hash SHA256 + * @return Document ou Optional.empty() + */ + public Optional findByHashSha256(String hashSha256) { + return find("hashSha256 = ?1 AND actif = true", hashSha256).firstResultOptional(); + } + + /** + * Trouve les documents par type + * + * @param type Type de document + * @return Liste des documents + */ + public List findByType(TypeDocument type) { + return find("typeDocument = ?1 AND actif = true ORDER BY dateCreation DESC", type).list(); + } + + /** + * Trouve tous les documents actifs + * + * @return Liste des documents actifs + */ + public List findAllActifs() { + return find("actif = true ORDER BY dateCreation DESC").list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/EcritureComptableRepository.java b/src/main/java/dev/lions/unionflow/server/repository/EcritureComptableRepository.java new file mode 100644 index 0000000..e72327a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/EcritureComptableRepository.java @@ -0,0 +1,109 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.EcritureComptable; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité EcritureComptable + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class EcritureComptableRepository implements PanacheRepository { + + /** + * Trouve une écriture comptable par son UUID + * + * @param id UUID de l'écriture comptable + * @return Écriture comptable ou Optional.empty() + */ + public Optional findEcritureComptableById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve une écriture par son numéro de pièce + * + * @param numeroPiece Numéro de pièce + * @return Écriture ou Optional.empty() + */ + public Optional findByNumeroPiece(String numeroPiece) { + return find("numeroPiece = ?1 AND actif = true", numeroPiece).firstResultOptional(); + } + + /** + * Trouve les écritures d'un journal + * + * @param journalId ID du journal + * @return Liste des écritures + */ + public List findByJournalId(UUID journalId) { + return find("journal.id = ?1 AND actif = true ORDER BY dateEcriture DESC, numeroPiece ASC", journalId) + .list(); + } + + /** + * Trouve les écritures d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des écritures + */ + public List findByOrganisationId(UUID organisationId) { + return find( + "organisation.id = ?1 AND actif = true ORDER BY dateEcriture DESC, numeroPiece ASC", + organisationId) + .list(); + } + + /** + * Trouve les écritures d'un paiement + * + * @param paiementId ID du paiement + * @return Liste des écritures + */ + public List findByPaiementId(UUID paiementId) { + return find("paiement.id = ?1 AND actif = true ORDER BY dateEcriture DESC", paiementId).list(); + } + + /** + * Trouve les écritures dans une période + * + * @param dateDebut Date de début + * @param dateFin Date de fin + * @return Liste des écritures + */ + public List findByPeriode(LocalDate dateDebut, LocalDate dateFin) { + return find( + "dateEcriture >= ?1 AND dateEcriture <= ?2 AND actif = true ORDER BY dateEcriture DESC, numeroPiece ASC", + dateDebut, + dateFin) + .list(); + } + + /** + * Trouve les écritures non pointées + * + * @return Liste des écritures non pointées + */ + public List findNonPointees() { + return find("pointe = false AND actif = true ORDER BY dateEcriture ASC").list(); + } + + /** + * Trouve les écritures avec un lettrage spécifique + * + * @param lettrage Lettrage + * @return Liste des écritures + */ + public List findByLettrage(String lettrage) { + return find("lettrage = ?1 AND actif = true ORDER BY dateEcriture DESC", lettrage).list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/EvenementRepository.java b/src/main/java/dev/lions/unionflow/server/repository/EvenementRepository.java new file mode 100644 index 0000000..62496b5 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/EvenementRepository.java @@ -0,0 +1,463 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.Evenement; +import dev.lions.unionflow.server.entity.Evenement.StatutEvenement; +import dev.lions.unionflow.server.entity.Evenement.TypeEvenement; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Événement avec UUID + * + *

Fournit les méthodes d'accès aux données pour la gestion des événements avec des + * fonctionnalités de recherche avancées et de filtrage. + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +@ApplicationScoped +public class EvenementRepository extends BaseRepository { + + public EvenementRepository() { + super(Evenement.class); + } + + /** + * Trouve un événement par son titre (recherche exacte) + * + * @param titre le titre de l'événement + * @return l'événement trouvé ou Optional.empty() + */ + public Optional findByTitre(String titre) { + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.titre = :titre", Evenement.class); + query.setParameter("titre", titre); + return query.getResultStream().findFirst(); + } + + /** + * Trouve tous les événements actifs + * + * @return la liste des événements actifs + */ + public List findAllActifs() { + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.actif = true", Evenement.class); + return query.getResultList(); + } + + /** + * Trouve tous les événements actifs avec pagination et tri + * + * @param page la page demandée + * @param sort le tri à appliquer + * @return la liste paginée des événements actifs + */ + public List findAllActifs(Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.actif = true" + orderBy, Evenement.class); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Compte le nombre d'événements actifs + * + * @return le nombre d'événements actifs + */ + public long countActifs() { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e WHERE e.actif = true", Long.class); + return query.getSingleResult(); + } + + /** + * Trouve les événements par statut + * + * @param statut le statut recherché + * @return la liste des événements avec ce statut + */ + public List findByStatut(StatutEvenement statut) { + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.statut = :statut", Evenement.class); + query.setParameter("statut", statut); + return query.getResultList(); + } + + /** + * Trouve les événements par statut avec pagination et tri + * + * @param statut le statut recherché + * @param page la page demandée + * @param sort le tri à appliquer + * @return la liste paginée des événements avec ce statut + */ + public List findByStatut(StatutEvenement statut, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.statut = :statut" + orderBy, Evenement.class); + query.setParameter("statut", statut); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les événements par type + * + * @param type le type d'événement recherché + * @return la liste des événements de ce type + */ + public List findByType(TypeEvenement type) { + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.typeEvenement = :type", Evenement.class); + query.setParameter("type", type); + return query.getResultList(); + } + + /** + * Trouve les événements par type avec pagination et tri + * + * @param type le type d'événement recherché + * @param page la page demandée + * @param sort le tri à appliquer + * @return la liste paginée des événements de ce type + */ + public List findByType(TypeEvenement type, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.typeEvenement = :type" + orderBy, Evenement.class); + query.setParameter("type", type); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les événements par organisation + * + * @param organisationId l'UUID de l'organisation + * @return la liste des événements de cette organisation + */ + public List findByOrganisation(UUID organisationId) { + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.organisation.id = :organisationId", Evenement.class); + query.setParameter("organisationId", organisationId); + return query.getResultList(); + } + + /** + * Trouve les événements par organisation avec pagination et tri + * + * @param organisationId l'UUID de l'organisation + * @param page la page demandée + * @param sort le tri à appliquer + * @return la liste paginée des événements de cette organisation + */ + public List findByOrganisation(UUID organisationId, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.organisation.id = :organisationId" + orderBy, + Evenement.class); + query.setParameter("organisationId", organisationId); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les événements à venir (date de début future) + * + * @return la liste des événements à venir + */ + public List findEvenementsAVenir() { + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.dateDebut > :maintenant AND e.actif = true", + Evenement.class); + query.setParameter("maintenant", LocalDateTime.now()); + return query.getResultList(); + } + + /** + * Trouve les événements à venir avec pagination et tri + * + * @param page la page demandée + * @param sort le tri à appliquer + * @return la liste paginée des événements à venir + */ + public List findEvenementsAVenir(Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.dateDebut > :maintenant AND e.actif = true" + orderBy, + Evenement.class); + query.setParameter("maintenant", LocalDateTime.now()); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les événements visibles au public + * + * @return la liste des événements publics + */ + public List findEvenementsPublics() { + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.visiblePublic = true AND e.actif = true", + Evenement.class); + return query.getResultList(); + } + + /** + * Trouve les événements visibles au public avec pagination et tri + * + * @param page la page demandée + * @param sort le tri à appliquer + * @return la liste paginée des événements publics + */ + public List findEvenementsPublics(Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT e FROM Evenement e WHERE e.visiblePublic = true AND e.actif = true" + orderBy, + Evenement.class); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Recherche avancée d'événements avec filtres multiples + * + * @param recherche terme de recherche (titre, description) + * @param statut statut de l'événement (optionnel) + * @param type type d'événement (optionnel) + * @param organisationId UUID de l'organisation (optionnel) + * @param organisateurId UUID de l'organisateur (optionnel) + * @param dateDebutMin date de début minimum (optionnel) + * @param dateDebutMax date de début maximum (optionnel) + * @param visiblePublic visibilité publique (optionnel) + * @param inscriptionRequise inscription requise (optionnel) + * @param actif statut actif (optionnel) + * @param page pagination + * @param sort tri + * @return la liste paginée des événements correspondants aux critères + */ + public List rechercheAvancee( + String recherche, + StatutEvenement statut, + TypeEvenement type, + UUID organisationId, + UUID organisateurId, + LocalDateTime dateDebutMin, + LocalDateTime dateDebutMax, + Boolean visiblePublic, + Boolean inscriptionRequise, + Boolean actif, + Page page, + Sort sort) { + StringBuilder jpql = new StringBuilder("SELECT e FROM Evenement e WHERE 1=1"); + Map params = new HashMap<>(); + + if (recherche != null && !recherche.trim().isEmpty()) { + jpql.append( + " AND (LOWER(e.titre) LIKE LOWER(:recherche) OR LOWER(e.description) LIKE LOWER(:recherche) OR LOWER(e.lieu) LIKE LOWER(:recherche))"); + params.put("recherche", "%" + recherche.toLowerCase() + "%"); + } + + if (statut != null) { + jpql.append(" AND e.statut = :statut"); + params.put("statut", statut); + } + + if (type != null) { + jpql.append(" AND e.typeEvenement = :type"); + params.put("type", type); + } + + if (organisationId != null) { + jpql.append(" AND e.organisation.id = :organisationId"); + params.put("organisationId", organisationId); + } + + if (organisateurId != null) { + jpql.append(" AND e.organisateur.id = :organisateurId"); + params.put("organisateurId", organisateurId); + } + + if (dateDebutMin != null) { + jpql.append(" AND e.dateDebut >= :dateDebutMin"); + params.put("dateDebutMin", dateDebutMin); + } + + if (dateDebutMax != null) { + jpql.append(" AND e.dateDebut <= :dateDebutMax"); + params.put("dateDebutMax", dateDebutMax); + } + + if (visiblePublic != null) { + jpql.append(" AND e.visiblePublic = :visiblePublic"); + params.put("visiblePublic", visiblePublic); + } + + if (inscriptionRequise != null) { + jpql.append(" AND e.inscriptionRequise = :inscriptionRequise"); + params.put("inscriptionRequise", inscriptionRequise); + } + + if (actif != null) { + jpql.append(" AND e.actif = :actif"); + params.put("actif", actif); + } + + if (sort != null) { + jpql.append(" ORDER BY ").append(buildOrderBy(sort)); + } + + TypedQuery query = entityManager.createQuery(jpql.toString(), Evenement.class); + for (Map.Entry param : params.entrySet()) { + query.setParameter(param.getKey(), param.getValue()); + } + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Obtient les statistiques des événements + * + * @return une map contenant les statistiques + */ + public Map getStatistiques() { + Map stats = new HashMap<>(); + LocalDateTime maintenant = LocalDateTime.now(); + + TypedQuery totalQuery = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e", Long.class); + stats.put("total", totalQuery.getSingleResult()); + + TypedQuery actifsQuery = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e WHERE e.actif = true", Long.class); + stats.put("actifs", actifsQuery.getSingleResult()); + + TypedQuery inactifsQuery = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e WHERE e.actif = false", Long.class); + stats.put("inactifs", inactifsQuery.getSingleResult()); + + TypedQuery aVenirQuery = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e WHERE e.dateDebut > :maintenant AND e.actif = true", + Long.class); + aVenirQuery.setParameter("maintenant", maintenant); + stats.put("aVenir", aVenirQuery.getSingleResult()); + + TypedQuery enCoursQuery = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e WHERE e.dateDebut <= :maintenant AND (e.dateFin IS NULL OR e.dateFin >= :maintenant) AND e.actif = true", + Long.class); + enCoursQuery.setParameter("maintenant", maintenant); + stats.put("enCours", enCoursQuery.getSingleResult()); + + TypedQuery passesQuery = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e WHERE (e.dateFin < :maintenant OR (e.dateFin IS NULL AND e.dateDebut < :maintenant)) AND e.actif = true", + Long.class); + passesQuery.setParameter("maintenant", maintenant); + stats.put("passes", passesQuery.getSingleResult()); + + TypedQuery publicsQuery = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e WHERE e.visiblePublic = true AND e.actif = true", + Long.class); + stats.put("publics", publicsQuery.getSingleResult()); + + TypedQuery avecInscriptionQuery = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e WHERE e.inscriptionRequise = true AND e.actif = true", + Long.class); + stats.put("avecInscription", avecInscriptionQuery.getSingleResult()); + + return stats; + } + + /** + * Compte les événements dans une période et organisation + * + * @param organisationId UUID de l'organisation + * @param debut date de début + * @param fin date de fin + * @return nombre d'événements + */ + public long countEvenements(UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(e) FROM Evenement e WHERE e.organisation.id = :organisationId AND e.dateDebut BETWEEN :debut AND :fin", + Long.class); + query.setParameter("organisationId", organisationId); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + return query.getSingleResult(); + } + + /** + * Calcule la moyenne de participants dans une période et organisation + * + * @param organisationId UUID de l'organisation + * @param debut date de début + * @param fin date de fin + * @return moyenne de participants ou null + */ + public Double calculerMoyenneParticipants(UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT AVG(e.nombreParticipants) FROM Evenement e WHERE e.organisation.id = :organisationId AND e.dateDebut BETWEEN :debut AND :fin", + Double.class); + query.setParameter("organisationId", organisationId); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + return query.getSingleResult(); + } + + /** + * Compte le total des participations dans une période et organisation + * + * @param organisationId UUID de l'organisation + * @param debut date de début + * @param fin date de fin + * @return total des participations + */ + public Long countTotalParticipations(UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT COALESCE(SUM(e.nombreParticipants), 0) FROM Evenement e WHERE e.organisation.id = :organisationId AND e.dateDebut BETWEEN :debut AND :fin", + Long.class); + query.setParameter("organisationId", organisationId); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + Long result = query.getSingleResult(); + return result != null ? result : 0L; + } + + /** Construit la clause ORDER BY à partir d'un Sort */ + private String buildOrderBy(Sort sort) { + if (sort == null || sort.getColumns().isEmpty()) { + return "e.dateDebut"; + } + StringBuilder orderBy = new StringBuilder(); + for (int i = 0; i < sort.getColumns().size(); i++) { + if (i > 0) { + orderBy.append(", "); + } + Sort.Column column = sort.getColumns().get(i); + orderBy.append("e.").append(column.getName()); + if (column.getDirection() == Sort.Direction.Descending) { + orderBy.append(" DESC"); + } else { + orderBy.append(" ASC"); + } + } + return orderBy.toString(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/JournalComptableRepository.java b/src/main/java/dev/lions/unionflow/server/repository/JournalComptableRepository.java new file mode 100644 index 0000000..9972e23 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/JournalComptableRepository.java @@ -0,0 +1,83 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.comptabilite.TypeJournalComptable; +import dev.lions.unionflow.server.entity.JournalComptable; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité JournalComptable + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class JournalComptableRepository implements PanacheRepository { + + /** + * Trouve un journal comptable par son UUID + * + * @param id UUID du journal comptable + * @return Journal comptable ou Optional.empty() + */ + public Optional findJournalComptableById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve un journal par son code + * + * @param code Code du journal + * @return Journal ou Optional.empty() + */ + public Optional findByCode(String code) { + return find("code = ?1 AND actif = true", code).firstResultOptional(); + } + + /** + * Trouve les journaux par type + * + * @param type Type de journal + * @return Liste des journaux + */ + public List findByType(TypeJournalComptable type) { + return find("typeJournal = ?1 AND actif = true ORDER BY code ASC", type).list(); + } + + /** + * Trouve les journaux ouverts + * + * @return Liste des journaux ouverts + */ + public List findJournauxOuverts() { + return find("statut = ?1 AND actif = true ORDER BY code ASC", "OUVERT").list(); + } + + /** + * Trouve les journaux pour une date donnée + * + * @param date Date à vérifier + * @return Liste des journaux actifs pour cette date + */ + public List findJournauxPourDate(LocalDate date) { + return find( + "(dateDebut IS NULL OR dateDebut <= ?1) AND (dateFin IS NULL OR dateFin >= ?1) AND actif = true ORDER BY code ASC", + date) + .list(); + } + + /** + * Trouve tous les journaux actifs + * + * @return Liste des journaux actifs + */ + public List findAllActifs() { + return find("actif = true ORDER BY code ASC").list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/LigneEcritureRepository.java b/src/main/java/dev/lions/unionflow/server/repository/LigneEcritureRepository.java new file mode 100644 index 0000000..381ac39 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/LigneEcritureRepository.java @@ -0,0 +1,51 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.LigneEcriture; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité LigneEcriture + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class LigneEcritureRepository implements PanacheRepository { + + /** + * Trouve une ligne d'écriture par son UUID + * + * @param id UUID de la ligne d'écriture + * @return Ligne d'écriture ou Optional.empty() + */ + public Optional findLigneEcritureById(UUID id) { + return find("id = ?1", id).firstResultOptional(); + } + + /** + * Trouve toutes les lignes d'une écriture + * + * @param ecritureId ID de l'écriture + * @return Liste des lignes + */ + public List findByEcritureId(UUID ecritureId) { + return find("ecriture.id = ?1 ORDER BY numeroLigne ASC", ecritureId).list(); + } + + /** + * Trouve toutes les lignes d'un compte comptable + * + * @param compteComptableId ID du compte comptable + * @return Liste des lignes + */ + public List findByCompteComptableId(UUID compteComptableId) { + return find("compteComptable.id = ?1 ORDER BY ecriture.dateEcriture DESC, numeroLigne ASC", compteComptableId) + .list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/MembreRepository.java b/src/main/java/dev/lions/unionflow/server/repository/MembreRepository.java new file mode 100644 index 0000000..0161854 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/MembreRepository.java @@ -0,0 +1,238 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.Membre; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** Repository pour l'entité Membre avec UUID */ +@ApplicationScoped +public class MembreRepository extends BaseRepository { + + public MembreRepository() { + super(Membre.class); + } + + /** Trouve un membre par son email */ + public Optional findByEmail(String email) { + TypedQuery query = entityManager.createQuery( + "SELECT m FROM Membre m WHERE m.email = :email", Membre.class); + query.setParameter("email", email); + return query.getResultStream().findFirst(); + } + + /** Trouve un membre par son numéro */ + public Optional findByNumeroMembre(String numeroMembre) { + TypedQuery query = entityManager.createQuery( + "SELECT m FROM Membre m WHERE m.numeroMembre = :numeroMembre", Membre.class); + query.setParameter("numeroMembre", numeroMembre); + return query.getResultStream().findFirst(); + } + + /** Trouve tous les membres actifs */ + public List findAllActifs() { + TypedQuery query = entityManager.createQuery( + "SELECT m FROM Membre m WHERE m.actif = true", Membre.class); + return query.getResultList(); + } + + /** Compte le nombre de membres actifs */ + public long countActifs() { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(m) FROM Membre m WHERE m.actif = true", Long.class); + return query.getSingleResult(); + } + + /** Trouve les membres par nom ou prénom (recherche partielle) */ + public List findByNomOrPrenom(String recherche) { + TypedQuery query = entityManager.createQuery( + "SELECT m FROM Membre m WHERE LOWER(m.nom) LIKE LOWER(:recherche) OR LOWER(m.prenom) LIKE LOWER(:recherche)", + Membre.class); + query.setParameter("recherche", "%" + recherche + "%"); + return query.getResultList(); + } + + /** Trouve tous les membres actifs avec pagination et tri */ + public List findAllActifs(Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT m FROM Membre m WHERE m.actif = true" + orderBy, Membre.class); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** Trouve les membres par nom ou prénom avec pagination et tri */ + public List findByNomOrPrenom(String recherche, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT m FROM Membre m WHERE LOWER(m.nom) LIKE LOWER(:recherche) OR LOWER(m.prenom) LIKE LOWER(:recherche)" + orderBy, + Membre.class); + query.setParameter("recherche", "%" + recherche + "%"); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** Compte les nouveaux membres depuis une date donnée */ + public long countNouveauxMembres(LocalDate depuis) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(m) FROM Membre m WHERE m.dateAdhesion >= :depuis", Long.class); + query.setParameter("depuis", depuis); + return query.getSingleResult(); + } + + /** Trouve les membres par statut avec pagination */ + public List findByStatut(boolean actif, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT m FROM Membre m WHERE m.actif = :actif" + orderBy, Membre.class); + query.setParameter("actif", actif); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** Trouve les membres par tranche d'âge */ + public List findByTrancheAge(int ageMin, int ageMax, Page page, Sort sort) { + LocalDate dateNaissanceMax = LocalDate.now().minusYears(ageMin); + LocalDate dateNaissanceMin = LocalDate.now().minusYears(ageMax + 1); + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT m FROM Membre m WHERE m.dateNaissance BETWEEN :dateMin AND :dateMax" + orderBy, + Membre.class); + query.setParameter("dateMin", dateNaissanceMin); + query.setParameter("dateMax", dateNaissanceMax); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** Recherche avancée de membres */ + public List rechercheAvancee( + String recherche, + Boolean actif, + LocalDate dateAdhesionMin, + LocalDate dateAdhesionMax, + Page page, + Sort sort) { + StringBuilder jpql = new StringBuilder("SELECT m FROM Membre m WHERE 1=1"); + + if (recherche != null && !recherche.isEmpty()) { + jpql.append(" AND (LOWER(m.nom) LIKE LOWER(:recherche) OR LOWER(m.prenom) LIKE LOWER(:recherche) OR LOWER(m.email) LIKE LOWER(:recherche))"); + } + if (actif != null) { + jpql.append(" AND m.actif = :actif"); + } + if (dateAdhesionMin != null) { + jpql.append(" AND m.dateAdhesion >= :dateAdhesionMin"); + } + if (dateAdhesionMax != null) { + jpql.append(" AND m.dateAdhesion <= :dateAdhesionMax"); + } + + if (sort != null) { + jpql.append(" ORDER BY ").append(buildOrderBy(sort)); + } + + TypedQuery query = entityManager.createQuery(jpql.toString(), Membre.class); + + if (recherche != null && !recherche.isEmpty()) { + query.setParameter("recherche", "%" + recherche + "%"); + } + if (actif != null) { + query.setParameter("actif", actif); + } + if (dateAdhesionMin != null) { + query.setParameter("dateAdhesionMin", dateAdhesionMin); + } + if (dateAdhesionMax != null) { + query.setParameter("dateAdhesionMax", dateAdhesionMax); + } + + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** Construit la clause ORDER BY à partir d'un Sort */ + private String buildOrderBy(Sort sort) { + if (sort == null || sort.getColumns().isEmpty()) { + return "m.id"; + } + StringBuilder orderBy = new StringBuilder(); + for (int i = 0; i < sort.getColumns().size(); i++) { + if (i > 0) { + orderBy.append(", "); + } + Sort.Column column = sort.getColumns().get(i); + orderBy.append("m.").append(column.getName()); + if (column.getDirection() == Sort.Direction.Descending) { + orderBy.append(" DESC"); + } else { + orderBy.append(" ASC"); + } + } + return orderBy.toString(); + } + + /** + * Compte les membres actifs dans une période et organisation + * + * @param organisationId UUID de l'organisation + * @param debut Date de début + * @param fin Date de fin + * @return Nombre de membres actifs + */ + public Long countMembresActifs(UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(m) FROM Membre m WHERE m.organisation.id = :organisationId AND m.actif = true AND m.dateAdhesion BETWEEN :debut AND :fin", + Long.class); + query.setParameter("organisationId", organisationId); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + return query.getSingleResult(); + } + + /** + * Compte les membres inactifs dans une période et organisation + * + * @param organisationId UUID de l'organisation + * @param debut Date de début + * @param fin Date de fin + * @return Nombre de membres inactifs + */ + public Long countMembresInactifs(UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(m) FROM Membre m WHERE m.organisation.id = :organisationId AND m.actif = false AND m.dateAdhesion BETWEEN :debut AND :fin", + Long.class); + query.setParameter("organisationId", organisationId); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + return query.getSingleResult(); + } + + /** + * Calcule la moyenne d'âge des membres dans une période et organisation + * + * @param organisationId UUID de l'organisation + * @param debut Date de début + * @param fin Date de fin + * @return Moyenne d'âge ou null si aucun membre + */ + public Double calculerMoyenneAge(UUID organisationId, LocalDateTime debut, LocalDateTime fin) { + TypedQuery query = entityManager.createQuery( + "SELECT AVG(YEAR(CURRENT_DATE) - YEAR(m.dateNaissance)) FROM Membre m WHERE m.organisation.id = :organisationId AND m.dateAdhesion BETWEEN :debut AND :fin", + Double.class); + query.setParameter("organisationId", organisationId); + query.setParameter("debut", debut); + query.setParameter("fin", fin); + return query.getSingleResult(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/MembreRoleRepository.java b/src/main/java/dev/lions/unionflow/server/repository/MembreRoleRepository.java new file mode 100644 index 0000000..5a1ba7c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/MembreRoleRepository.java @@ -0,0 +1,77 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.MembreRole; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité MembreRole + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class MembreRoleRepository implements PanacheRepository { + + /** + * Trouve une attribution membre-role par son UUID + * + * @param id UUID de l'attribution + * @return Attribution ou Optional.empty() + */ + public Optional findMembreRoleById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve tous les rôles d'un membre + * + * @param membreId ID du membre + * @return Liste des attributions de rôles + */ + public List findByMembreId(UUID membreId) { + return find("membre.id = ?1 AND actif = true", membreId).list(); + } + + /** + * Trouve tous les rôles actifs d'un membre (dans la période valide) + * + * @param membreId ID du membre + * @return Liste des attributions de rôles actives + */ + public List findActifsByMembreId(UUID membreId) { + LocalDate aujourdhui = LocalDate.now(); + return find( + "membre.id = ?1 AND actif = true AND (dateDebut IS NULL OR dateDebut <= ?2) AND (dateFin IS NULL OR dateFin >= ?2)", + membreId, + aujourdhui) + .list(); + } + + /** + * Trouve tous les membres ayant un rôle spécifique + * + * @param roleId ID du rôle + * @return Liste des attributions de rôles + */ + public List findByRoleId(UUID roleId) { + return find("role.id = ?1 AND actif = true", roleId).list(); + } + + /** + * Trouve une attribution spécifique membre-role + * + * @param membreId ID du membre + * @param roleId ID du rôle + * @return Attribution ou null + */ + public MembreRole findByMembreAndRole(UUID membreId, UUID roleId) { + return find("membre.id = ?1 AND role.id = ?2", membreId, roleId).firstResult(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/NotificationRepository.java b/src/main/java/dev/lions/unionflow/server/repository/NotificationRepository.java new file mode 100644 index 0000000..a8e22c9 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/NotificationRepository.java @@ -0,0 +1,127 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.notification.PrioriteNotification; +import dev.lions.unionflow.server.api.enums.notification.StatutNotification; +import dev.lions.unionflow.server.api.enums.notification.TypeNotification; +import dev.lions.unionflow.server.entity.Notification; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Notification + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class NotificationRepository implements PanacheRepository { + + /** + * Trouve une notification par son UUID + * + * @param id UUID de la notification + * @return Notification ou Optional.empty() + */ + public Optional findNotificationById(UUID id) { + return find("id = ?1", id).firstResultOptional(); + } + + /** + * Trouve toutes les notifications d'un membre + * + * @param membreId ID du membre + * @return Liste des notifications + */ + public List findByMembreId(UUID membreId) { + return find("membre.id = ?1 ORDER BY dateEnvoiPrevue DESC, dateCreation DESC", membreId).list(); + } + + /** + * Trouve toutes les notifications non lues d'un membre + * + * @param membreId ID du membre + * @return Liste des notifications non lues + */ + public List findNonLuesByMembreId(UUID membreId) { + return find( + "membre.id = ?1 AND statut = ?2 ORDER BY priorite ASC, dateEnvoiPrevue DESC", + membreId, + StatutNotification.NON_LUE) + .list(); + } + + /** + * Trouve toutes les notifications d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des notifications + */ + public List findByOrganisationId(UUID organisationId) { + return find("organisation.id = ?1 ORDER BY dateEnvoiPrevue DESC, dateCreation DESC", organisationId) + .list(); + } + + /** + * Trouve les notifications par type + * + * @param type Type de notification + * @return Liste des notifications + */ + public List findByType(TypeNotification type) { + return find("typeNotification = ?1 ORDER BY dateEnvoiPrevue DESC", type).list(); + } + + /** + * Trouve les notifications par statut + * + * @param statut Statut de la notification + * @return Liste des notifications + */ + public List findByStatut(StatutNotification statut) { + return find("statut = ?1 ORDER BY dateEnvoiPrevue DESC", statut).list(); + } + + /** + * Trouve les notifications par priorité + * + * @param priorite Priorité de la notification + * @return Liste des notifications + */ + public List findByPriorite(PrioriteNotification priorite) { + return find("priorite = ?1 ORDER BY dateEnvoiPrevue DESC", priorite).list(); + } + + /** + * Trouve les notifications en attente d'envoi + * + * @return Liste des notifications en attente + */ + public List findEnAttenteEnvoi() { + LocalDateTime maintenant = LocalDateTime.now(); + return find( + "statut IN (?1, ?2) AND dateEnvoiPrevue <= ?3 ORDER BY priorite DESC, dateEnvoiPrevue ASC", + StatutNotification.EN_ATTENTE, + StatutNotification.PROGRAMMEE, + maintenant) + .list(); + } + + /** + * Trouve les notifications échouées pouvant être retentées + * + * @return Liste des notifications échouées + */ + public List findEchoueesRetentables() { + return find( + "statut IN (?1, ?2) AND (nombreTentatives IS NULL OR nombreTentatives < 5) ORDER BY dateEnvoiPrevue ASC", + StatutNotification.ECHEC_ENVOI, + StatutNotification.ERREUR_TECHNIQUE) + .list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/OrganisationRepository.java b/src/main/java/dev/lions/unionflow/server/repository/OrganisationRepository.java new file mode 100644 index 0000000..e935553 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/OrganisationRepository.java @@ -0,0 +1,424 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.Organisation; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Organisation avec UUID + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-16 + */ +@ApplicationScoped +public class OrganisationRepository extends BaseRepository { + + public OrganisationRepository() { + super(Organisation.class); + } + + /** + * Trouve une organisation par son email + * + * @param email l'email de l'organisation + * @return Optional contenant l'organisation si trouvée + */ + public Optional findByEmail(String email) { + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.email = :email", Organisation.class); + query.setParameter("email", email); + return query.getResultStream().findFirst(); + } + + /** + * Trouve une organisation par son nom + * + * @param nom le nom de l'organisation + * @return Optional contenant l'organisation si trouvée + */ + public Optional findByNom(String nom) { + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.nom = :nom", Organisation.class); + query.setParameter("nom", nom); + return query.getResultStream().findFirst(); + } + + /** + * Trouve une organisation par son numéro d'enregistrement + * + * @param numeroEnregistrement le numéro d'enregistrement officiel + * @return Optional contenant l'organisation si trouvée + */ + public Optional findByNumeroEnregistrement(String numeroEnregistrement) { + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.numeroEnregistrement = :numeroEnregistrement", + Organisation.class); + query.setParameter("numeroEnregistrement", numeroEnregistrement); + return query.getResultStream().findFirst(); + } + + /** + * Trouve toutes les organisations actives + * + * @return liste des organisations actives + */ + public List findAllActives() { + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.statut = 'ACTIVE' AND o.actif = true", + Organisation.class); + return query.getResultList(); + } + + /** + * Trouve toutes les organisations actives avec pagination + * + * @param page pagination + * @param sort tri + * @return liste paginée des organisations actives + */ + public List findAllActives(Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.statut = 'ACTIVE' AND o.actif = true" + orderBy, + Organisation.class); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Compte le nombre d'organisations actives + * + * @return nombre d'organisations actives + */ + public long countActives() { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(o) FROM Organisation o WHERE o.statut = 'ACTIVE' AND o.actif = true", + Long.class); + return query.getSingleResult(); + } + + /** + * Trouve les organisations par statut + * + * @param statut le statut recherché + * @param page pagination + * @param sort tri + * @return liste paginée des organisations avec le statut spécifié + */ + public List findByStatut(String statut, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.statut = :statut" + orderBy, + Organisation.class); + query.setParameter("statut", statut); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les organisations par type + * + * @param typeOrganisation le type d'organisation + * @param page pagination + * @param sort tri + * @return liste paginée des organisations du type spécifié + */ + public List findByType(String typeOrganisation, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.typeOrganisation = :typeOrganisation" + orderBy, + Organisation.class); + query.setParameter("typeOrganisation", typeOrganisation); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les organisations par ville + * + * @param ville la ville + * @param page pagination + * @param sort tri + * @return liste paginée des organisations de la ville spécifiée + */ + public List findByVille(String ville, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.ville = :ville" + orderBy, + Organisation.class); + query.setParameter("ville", ville); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les organisations par pays + * + * @param pays le pays + * @param page pagination + * @param sort tri + * @return liste paginée des organisations du pays spécifié + */ + public List findByPays(String pays, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.pays = :pays" + orderBy, + Organisation.class); + query.setParameter("pays", pays); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les organisations par région + * + * @param region la région + * @param page pagination + * @param sort tri + * @return liste paginée des organisations de la région spécifiée + */ + public List findByRegion(String region, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.region = :region" + orderBy, + Organisation.class); + query.setParameter("region", region); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les organisations filles d'une organisation parente + * + * @param organisationParenteId l'UUID de l'organisation parente + * @param page pagination + * @param sort tri + * @return liste paginée des organisations filles + */ + public List findByOrganisationParente( + UUID organisationParenteId, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.organisationParenteId = :organisationParenteId" + + orderBy, + Organisation.class); + query.setParameter("organisationParenteId", organisationParenteId); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les organisations racines (sans parent) + * + * @param page pagination + * @param sort tri + * @return liste paginée des organisations racines + */ + public List findOrganisationsRacines(Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.organisationParenteId IS NULL" + orderBy, + Organisation.class); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Recherche d'organisations par nom ou nom court + * + * @param recherche terme de recherche + * @param page pagination + * @param sort tri + * @return liste paginée des organisations correspondantes + */ + public List findByNomOrNomCourt(String recherche, Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE LOWER(o.nom) LIKE LOWER(:recherche) OR LOWER(o.nomCourt) LIKE LOWER(:recherche)" + + orderBy, + Organisation.class); + query.setParameter("recherche", "%" + recherche + "%"); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Recherche avancée d'organisations + * + * @param nom nom (optionnel) + * @param typeOrganisation type (optionnel) + * @param statut statut (optionnel) + * @param ville ville (optionnel) + * @param region région (optionnel) + * @param pays pays (optionnel) + * @param page pagination + * @return liste filtrée des organisations + */ + public List rechercheAvancee( + String nom, + String typeOrganisation, + String statut, + String ville, + String region, + String pays, + Page page) { + StringBuilder queryBuilder = new StringBuilder("SELECT o FROM Organisation o WHERE 1=1"); + Map parameters = new HashMap<>(); + + if (nom != null && !nom.isEmpty()) { + queryBuilder.append(" AND (LOWER(o.nom) LIKE LOWER(:nom) OR LOWER(o.nomCourt) LIKE LOWER(:nom))"); + parameters.put("nom", "%" + nom.toLowerCase() + "%"); + } + + if (typeOrganisation != null && !typeOrganisation.isEmpty()) { + queryBuilder.append(" AND o.typeOrganisation = :typeOrganisation"); + parameters.put("typeOrganisation", typeOrganisation); + } + + if (statut != null && !statut.isEmpty()) { + queryBuilder.append(" AND o.statut = :statut"); + parameters.put("statut", statut); + } + + if (ville != null && !ville.isEmpty()) { + queryBuilder.append(" AND LOWER(o.ville) LIKE LOWER(:ville)"); + parameters.put("ville", "%" + ville.toLowerCase() + "%"); + } + + if (region != null && !region.isEmpty()) { + queryBuilder.append(" AND LOWER(o.region) LIKE LOWER(:region)"); + parameters.put("region", "%" + region.toLowerCase() + "%"); + } + + if (pays != null && !pays.isEmpty()) { + queryBuilder.append(" AND LOWER(o.pays) LIKE LOWER(:pays)"); + parameters.put("pays", "%" + pays.toLowerCase() + "%"); + } + + queryBuilder.append(" ORDER BY o.nom ASC"); + + TypedQuery query = entityManager.createQuery( + queryBuilder.toString(), Organisation.class); + for (Map.Entry param : parameters.entrySet()) { + query.setParameter(param.getKey(), param.getValue()); + } + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Compte les nouvelles organisations depuis une date donnée + * + * @param depuis date de référence + * @return nombre de nouvelles organisations + */ + public long countNouvellesOrganisations(LocalDate depuis) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(o) FROM Organisation o WHERE o.dateCreation >= :depuis", Long.class); + query.setParameter("depuis", depuis.atStartOfDay()); + return query.getSingleResult(); + } + + /** + * Trouve les organisations publiques (visibles dans l'annuaire) + * + * @param page pagination + * @param sort tri + * @return liste paginée des organisations publiques + */ + public List findOrganisationsPubliques(Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.organisationPublique = true AND o.statut = 'ACTIVE' AND o.actif = true" + + orderBy, + Organisation.class); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Trouve les organisations acceptant de nouveaux membres + * + * @param page pagination + * @param sort tri + * @return liste paginée des organisations acceptant de nouveaux membres + */ + public List findOrganisationsOuvertes(Page page, Sort sort) { + String orderBy = sort != null ? " ORDER BY " + buildOrderBy(sort) : ""; + TypedQuery query = entityManager.createQuery( + "SELECT o FROM Organisation o WHERE o.accepteNouveauxMembres = true AND o.statut = 'ACTIVE' AND o.actif = true" + + orderBy, + Organisation.class); + query.setFirstResult(page.index * page.size); + query.setMaxResults(page.size); + return query.getResultList(); + } + + /** + * Compte les organisations par statut + * + * @param statut le statut + * @return nombre d'organisations avec ce statut + */ + public long countByStatut(String statut) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(o) FROM Organisation o WHERE o.statut = :statut", Long.class); + query.setParameter("statut", statut); + return query.getSingleResult(); + } + + /** + * Compte les organisations par type + * + * @param typeOrganisation le type d'organisation + * @return nombre d'organisations de ce type + */ + public long countByType(String typeOrganisation) { + TypedQuery query = entityManager.createQuery( + "SELECT COUNT(o) FROM Organisation o WHERE o.typeOrganisation = :typeOrganisation", + Long.class); + query.setParameter("typeOrganisation", typeOrganisation); + return query.getSingleResult(); + } + + /** Construit la clause ORDER BY à partir d'un Sort */ + private String buildOrderBy(Sort sort) { + if (sort == null || sort.getColumns().isEmpty()) { + return "o.id"; + } + StringBuilder orderBy = new StringBuilder(); + for (int i = 0; i < sort.getColumns().size(); i++) { + if (i > 0) { + orderBy.append(", "); + } + Sort.Column column = sort.getColumns().get(i); + orderBy.append("o.").append(column.getName()); + if (column.getDirection() == Sort.Direction.Descending) { + orderBy.append(" DESC"); + } else { + orderBy.append(" ASC"); + } + } + return orderBy.toString(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/repository/PaiementRepository.java b/src/main/java/dev/lions/unionflow/server/repository/PaiementRepository.java new file mode 100644 index 0000000..a6f2af3 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/PaiementRepository.java @@ -0,0 +1,110 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.paiement.MethodePaiement; +import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement; +import dev.lions.unionflow.server.entity.Paiement; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Paiement + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class PaiementRepository implements PanacheRepository { + + /** + * Trouve un paiement par son UUID + * + * @param id UUID du paiement + * @return Paiement ou Optional.empty() + */ + public Optional findPaiementById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve un paiement par son numéro de référence + * + * @param numeroReference Numéro de référence + * @return Paiement ou Optional.empty() + */ + public Optional findByNumeroReference(String numeroReference) { + return find("numeroReference", numeroReference).firstResultOptional(); + } + + /** + * Trouve tous les paiements d'un membre + * + * @param membreId ID du membre + * @return Liste des paiements + */ + public List findByMembreId(UUID membreId) { + return find("membre.id = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), membreId) + .list(); + } + + /** + * Trouve les paiements par statut + * + * @param statut Statut du paiement + * @return Liste des paiements + */ + public List findByStatut(StatutPaiement statut) { + return find("statutPaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), statut) + .list(); + } + + /** + * Trouve les paiements par méthode + * + * @param methode Méthode de paiement + * @return Liste des paiements + */ + public List findByMethode(MethodePaiement methode) { + return find("methodePaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), methode) + .list(); + } + + /** + * Trouve les paiements validés dans une période + * + * @param dateDebut Date de début + * @param dateFin Date de fin + * @return Liste des paiements + */ + public List findValidesParPeriode(LocalDateTime dateDebut, LocalDateTime dateFin) { + return find( + "statutPaiement = ?1 AND dateValidation >= ?2 AND dateValidation <= ?3 AND actif = true", + Sort.by("dateValidation", Sort.Direction.Descending), + StatutPaiement.VALIDE, + dateDebut, + dateFin) + .list(); + } + + /** + * Calcule le montant total des paiements validés dans une période + * + * @param dateDebut Date de début + * @param dateFin Date de fin + * @return Montant total + */ + public BigDecimal calculerMontantTotalValides(LocalDateTime dateDebut, LocalDateTime dateFin) { + List paiements = findValidesParPeriode(dateDebut, dateFin); + return paiements.stream() + .map(Paiement::getMontant) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/PermissionRepository.java b/src/main/java/dev/lions/unionflow/server/repository/PermissionRepository.java new file mode 100644 index 0000000..bf7aaf7 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/PermissionRepository.java @@ -0,0 +1,87 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.Permission; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Permission + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class PermissionRepository implements PanacheRepository { + + /** + * Trouve une permission par son UUID + * + * @param id UUID de la permission + * @return Permission ou Optional.empty() + */ + public Optional findPermissionById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve une permission par son code + * + * @param code Code de la permission + * @return Permission ou Optional.empty() + */ + public Optional findByCode(String code) { + return find("code", code).firstResultOptional(); + } + + /** + * Trouve les permissions par module + * + * @param module Nom du module + * @return Liste des permissions + */ + public List findByModule(String module) { + return find("LOWER(module) = LOWER(?1) AND actif = true", module).list(); + } + + /** + * Trouve les permissions par ressource + * + * @param ressource Nom de la ressource + * @return Liste des permissions + */ + public List findByRessource(String ressource) { + return find("LOWER(ressource) = LOWER(?1) AND actif = true", ressource).list(); + } + + /** + * Trouve les permissions par module et ressource + * + * @param module Nom du module + * @param ressource Nom de la ressource + * @return Liste des permissions + */ + public List findByModuleAndRessource(String module, String ressource) { + return find( + "LOWER(module) = LOWER(?1) AND LOWER(ressource) = LOWER(?2) AND actif = true", + module, + ressource) + .list(); + } + + /** + * Trouve toutes les permissions actives + * + * @return Liste des permissions actives + */ + public List findAllActives() { + return find("actif = true", Sort.by("module", Sort.Direction.Ascending) + .and("ressource", Sort.Direction.Ascending) + .and("action", Sort.Direction.Ascending)).list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/PieceJointeRepository.java b/src/main/java/dev/lions/unionflow/server/repository/PieceJointeRepository.java new file mode 100644 index 0000000..db165e0 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/PieceJointeRepository.java @@ -0,0 +1,100 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.PieceJointe; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité PieceJointe + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class PieceJointeRepository implements PanacheRepository { + + /** + * Trouve une pièce jointe par son UUID + * + * @param id UUID de la pièce jointe + * @return Pièce jointe ou Optional.empty() + */ + public Optional findPieceJointeById(UUID id) { + return find("id = ?1", id).firstResultOptional(); + } + + /** + * Trouve toutes les pièces jointes d'un document + * + * @param documentId ID du document + * @return Liste des pièces jointes + */ + public List findByDocumentId(UUID documentId) { + return find("document.id = ?1 ORDER BY ordre ASC", documentId).list(); + } + + /** + * Trouve toutes les pièces jointes d'un membre + * + * @param membreId ID du membre + * @return Liste des pièces jointes + */ + public List findByMembreId(UUID membreId) { + return find("membre.id = ?1 ORDER BY ordre ASC", membreId).list(); + } + + /** + * Trouve toutes les pièces jointes d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des pièces jointes + */ + public List findByOrganisationId(UUID organisationId) { + return find("organisation.id = ?1 ORDER BY ordre ASC", organisationId).list(); + } + + /** + * Trouve toutes les pièces jointes d'une cotisation + * + * @param cotisationId ID de la cotisation + * @return Liste des pièces jointes + */ + public List findByCotisationId(UUID cotisationId) { + return find("cotisation.id = ?1 ORDER BY ordre ASC", cotisationId).list(); + } + + /** + * Trouve toutes les pièces jointes d'une adhésion + * + * @param adhesionId ID de l'adhésion + * @return Liste des pièces jointes + */ + public List findByAdhesionId(UUID adhesionId) { + return find("adhesion.id = ?1 ORDER BY ordre ASC", adhesionId).list(); + } + + /** + * Trouve toutes les pièces jointes d'une demande d'aide + * + * @param demandeAideId ID de la demande d'aide + * @return Liste des pièces jointes + */ + public List findByDemandeAideId(UUID demandeAideId) { + return find("demandeAide.id = ?1 ORDER BY ordre ASC", demandeAideId).list(); + } + + /** + * Trouve toutes les pièces jointes d'une transaction Wave + * + * @param transactionWaveId ID de la transaction Wave + * @return Liste des pièces jointes + */ + public List findByTransactionWaveId(UUID transactionWaveId) { + return find("transactionWave.id = ?1 ORDER BY ordre ASC", transactionWaveId).list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/RolePermissionRepository.java b/src/main/java/dev/lions/unionflow/server/repository/RolePermissionRepository.java new file mode 100644 index 0000000..7780d2b --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/RolePermissionRepository.java @@ -0,0 +1,61 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.RolePermission; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité RolePermission + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class RolePermissionRepository implements PanacheRepository { + + /** + * Trouve une association rôle-permission par son UUID + * + * @param id UUID de l'association + * @return Association ou Optional.empty() + */ + public Optional findRolePermissionById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve toutes les permissions d'un rôle + * + * @param roleId ID du rôle + * @return Liste des associations rôle-permission + */ + public List findByRoleId(UUID roleId) { + return find("role.id = ?1 AND actif = true", roleId).list(); + } + + /** + * Trouve tous les rôles ayant une permission spécifique + * + * @param permissionId ID de la permission + * @return Liste des associations rôle-permission + */ + public List findByPermissionId(UUID permissionId) { + return find("permission.id = ?1 AND actif = true", permissionId).list(); + } + + /** + * Trouve une association spécifique rôle-permission + * + * @param roleId ID du rôle + * @param permissionId ID de la permission + * @return Association ou null + */ + public RolePermission findByRoleAndPermission(UUID roleId, UUID permissionId) { + return find("role.id = ?1 AND permission.id = ?2", roleId, permissionId).firstResult(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/RoleRepository.java b/src/main/java/dev/lions/unionflow/server/repository/RoleRepository.java new file mode 100644 index 0000000..2102a0c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/RoleRepository.java @@ -0,0 +1,83 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.Role; +import dev.lions.unionflow.server.entity.Role.TypeRole; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité Role + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class RoleRepository implements PanacheRepository { + + /** + * Trouve un rôle par son UUID + * + * @param id UUID du rôle + * @return Rôle ou Optional.empty() + */ + public Optional findRoleById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve un rôle par son code + * + * @param code Code du rôle + * @return Rôle ou Optional.empty() + */ + public Optional findByCode(String code) { + return find("code", code).firstResultOptional(); + } + + /** + * Trouve tous les rôles système + * + * @return Liste des rôles système + */ + public List findRolesSysteme() { + return find("typeRole = ?1 AND actif = true", Sort.by("niveauHierarchique", Sort.Direction.Ascending), TypeRole.SYSTEME) + .list(); + } + + /** + * Trouve tous les rôles d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des rôles + */ + public List findByOrganisationId(UUID organisationId) { + return find("organisation.id = ?1 AND actif = true", Sort.by("niveauHierarchique", Sort.Direction.Ascending), organisationId) + .list(); + } + + /** + * Trouve tous les rôles actifs + * + * @return Liste des rôles actifs + */ + public List findAllActifs() { + return find("actif = true", Sort.by("niveauHierarchique", Sort.Direction.Ascending)).list(); + } + + /** + * Trouve les rôles par type + * + * @param typeRole Type de rôle + * @return Liste des rôles + */ + public List findByType(TypeRole typeRole) { + return find("typeRole = ?1 AND actif = true", Sort.by("niveauHierarchique", Sort.Direction.Ascending), typeRole) + .list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/TemplateNotificationRepository.java b/src/main/java/dev/lions/unionflow/server/repository/TemplateNotificationRepository.java new file mode 100644 index 0000000..b98da10 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/TemplateNotificationRepository.java @@ -0,0 +1,59 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.TemplateNotification; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité TemplateNotification + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class TemplateNotificationRepository implements PanacheRepository { + + /** + * Trouve un template par son UUID + * + * @param id UUID du template + * @return Template ou Optional.empty() + */ + public Optional findTemplateNotificationById(UUID id) { + return find("id = ?1 AND actif = true", id).firstResultOptional(); + } + + /** + * Trouve un template par son code + * + * @param code Code du template + * @return Template ou Optional.empty() + */ + public Optional findByCode(String code) { + return find("code = ?1 AND actif = true", code).firstResultOptional(); + } + + /** + * Trouve tous les templates actifs + * + * @return Liste des templates actifs + */ + public List findAllActifs() { + return find("actif = true ORDER BY code ASC").list(); + } + + /** + * Trouve les templates par langue + * + * @param langue Code langue (ex: fr, en) + * @return Liste des templates + */ + public List findByLangue(String langue) { + return find("langue = ?1 AND actif = true ORDER BY code ASC", langue).list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/TransactionWaveRepository.java b/src/main/java/dev/lions/unionflow/server/repository/TransactionWaveRepository.java new file mode 100644 index 0000000..1f5db53 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/TransactionWaveRepository.java @@ -0,0 +1,109 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave; +import dev.lions.unionflow.server.api.enums.wave.TypeTransactionWave; +import dev.lions.unionflow.server.entity.TransactionWave; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité TransactionWave + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class TransactionWaveRepository implements PanacheRepository { + + /** + * Trouve une transaction par son UUID + * + * @param id UUID de la transaction + * @return Transaction ou Optional.empty() + */ + public Optional findTransactionWaveById(UUID id) { + return find("id = ?1", id).firstResultOptional(); + } + + /** + * Trouve une transaction par son identifiant Wave + * + * @param waveTransactionId Identifiant Wave + * @return Transaction ou Optional.empty() + */ + public Optional findByWaveTransactionId(String waveTransactionId) { + return find("waveTransactionId = ?1", waveTransactionId).firstResultOptional(); + } + + /** + * Trouve une transaction par son identifiant de requête + * + * @param waveRequestId Identifiant de requête + * @return Transaction ou Optional.empty() + */ + public Optional findByWaveRequestId(String waveRequestId) { + return find("waveRequestId = ?1", waveRequestId).firstResultOptional(); + } + + /** + * Trouve toutes les transactions d'un compte Wave + * + * @param compteWaveId ID du compte Wave + * @return Liste des transactions + */ + public List findByCompteWaveId(UUID compteWaveId) { + return find("compteWave.id = ?1 ORDER BY dateCreation DESC", compteWaveId).list(); + } + + /** + * Trouve les transactions par statut + * + * @param statut Statut de la transaction + * @return Liste des transactions + */ + public List findByStatut(StatutTransactionWave statut) { + return find("statutTransaction = ?1 ORDER BY dateCreation DESC", statut).list(); + } + + /** + * Trouve les transactions par type + * + * @param type Type de transaction + * @return Liste des transactions + */ + public List findByType(TypeTransactionWave type) { + return find("typeTransaction = ?1 ORDER BY dateCreation DESC", type).list(); + } + + /** + * Trouve les transactions réussies dans une période + * + * @param compteWaveId ID du compte Wave + * @return Liste des transactions réussies + */ + public List findReussiesByCompteWave(UUID compteWaveId) { + return find( + "compteWave.id = ?1 AND statutTransaction = ?2 ORDER BY dateCreation DESC", + compteWaveId, + StatutTransactionWave.REUSSIE) + .list(); + } + + /** + * Trouve les transactions échouées pouvant être retentées + * + * @return Liste des transactions échouées + */ + public List findEchoueesRetentables() { + return find( + "statutTransaction IN (?1, ?2) AND (nombreTentatives IS NULL OR nombreTentatives < 5) ORDER BY dateCreation ASC", + StatutTransactionWave.ECHOUE, + StatutTransactionWave.EXPIRED) + .list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/repository/TypeOrganisationRepository.java b/src/main/java/dev/lions/unionflow/server/repository/TypeOrganisationRepository.java new file mode 100644 index 0000000..9b842e9 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/TypeOrganisationRepository.java @@ -0,0 +1,43 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.entity.TypeOrganisationEntity; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.TypedQuery; +import java.util.List; +import java.util.Optional; + +/** + * Repository pour l'entité {@link TypeOrganisationEntity}. + * + *

Permet de gérer le catalogue des types d'organisations. + */ +@ApplicationScoped +public class TypeOrganisationRepository extends BaseRepository { + + public TypeOrganisationRepository() { + super(TypeOrganisationEntity.class); + } + + /** Recherche un type par son code fonctionnel. */ + public Optional findByCode(String code) { + TypedQuery query = + entityManager.createQuery( + "SELECT t FROM TypeOrganisationEntity t WHERE UPPER(t.code) = UPPER(:code)", + TypeOrganisationEntity.class); + query.setParameter("code", code); + return query.getResultStream().findFirst(); + } + + /** Liste les types actifs, triés par ordreAffichage puis libellé. */ + public List listActifsOrdennes() { + return entityManager + .createQuery( + "SELECT t FROM TypeOrganisationEntity t " + + "WHERE t.actif = true " + + "ORDER BY COALESCE(t.ordreAffichage, 9999), t.libelle", + TypeOrganisationEntity.class) + .getResultList(); + } +} + + diff --git a/src/main/java/dev/lions/unionflow/server/repository/WebhookWaveRepository.java b/src/main/java/dev/lions/unionflow/server/repository/WebhookWaveRepository.java new file mode 100644 index 0000000..1164861 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/repository/WebhookWaveRepository.java @@ -0,0 +1,104 @@ +package dev.lions.unionflow.server.repository; + +import dev.lions.unionflow.server.api.enums.wave.StatutWebhook; +import dev.lions.unionflow.server.api.enums.wave.TypeEvenementWebhook; +import dev.lions.unionflow.server.entity.WebhookWave; +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Repository pour l'entité WebhookWave + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class WebhookWaveRepository implements PanacheRepository { + + /** + * Trouve un webhook Wave par son UUID + * + * @param id UUID du webhook + * @return Webhook ou Optional.empty() + */ + public Optional findWebhookWaveById(UUID id) { + return find("id = ?1", id).firstResultOptional(); + } + + /** + * Trouve un webhook par son identifiant d'événement Wave + * + * @param waveEventId Identifiant d'événement + * @return Webhook ou Optional.empty() + */ + public Optional findByWaveEventId(String waveEventId) { + return find("waveEventId = ?1", waveEventId).firstResultOptional(); + } + + /** + * Trouve tous les webhooks d'une transaction + * + * @param transactionWaveId ID de la transaction + * @return Liste des webhooks + */ + public List findByTransactionWaveId(UUID transactionWaveId) { + return find("transactionWave.id = ?1 ORDER BY dateReception DESC", transactionWaveId).list(); + } + + /** + * Trouve tous les webhooks d'un paiement + * + * @param paiementId ID du paiement + * @return Liste des webhooks + */ + public List findByPaiementId(UUID paiementId) { + return find("paiement.id = ?1 ORDER BY dateReception DESC", paiementId).list(); + } + + /** + * Trouve les webhooks par statut + * + * @param statut Statut de traitement + * @return Liste des webhooks + */ + public List findByStatut(StatutWebhook statut) { + return find("statutTraitement = ?1 ORDER BY dateReception DESC", statut).list(); + } + + /** + * Trouve les webhooks par type d'événement + * + * @param type Type d'événement + * @return Liste des webhooks + */ + public List findByType(TypeEvenementWebhook type) { + return find("typeEvenement = ?1 ORDER BY dateReception DESC", type).list(); + } + + /** + * Trouve les webhooks en attente de traitement + * + * @return Liste des webhooks en attente + */ + public List findEnAttente() { + return find("statutTraitement = ?1 ORDER BY dateReception ASC", StatutWebhook.EN_ATTENTE) + .list(); + } + + /** + * Trouve les webhooks échoués pouvant être retentés + * + * @return Liste des webhooks échoués + */ + public List findEchouesRetentables() { + return find( + "statutTraitement = ?1 AND (nombreTentatives IS NULL OR nombreTentatives < 5) ORDER BY dateReception ASC", + StatutWebhook.ECHOUE) + .list(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/resource/AdhesionResource.java b/src/main/java/dev/lions/unionflow/server/resource/AdhesionResource.java new file mode 100644 index 0000000..01cc320 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/AdhesionResource.java @@ -0,0 +1,705 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.finance.AdhesionDTO; +import dev.lions.unionflow.server.service.AdhesionService; +import jakarta.inject.Inject; +import jakarta.annotation.security.RolesAllowed; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * Resource REST pour la gestion des adhésions + * Expose les endpoints API pour les opérations CRUD sur les adhésions + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@Path("/api/adhesions") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Adhésions", description = "Gestion des demandes d'adhésion des membres") +@Slf4j +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class AdhesionResource { + + @Inject AdhesionService adhesionService; + + /** Récupère toutes les adhésions avec pagination */ + @GET + @Operation( + summary = "Lister toutes les adhésions", + description = "Récupère la liste paginée de toutes les adhésions") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Liste des adhésions récupérée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = AdhesionDTO.class))), + @APIResponse(responseCode = "400", description = "Paramètres de pagination invalides"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAllAdhesions( + @Parameter(description = "Numéro de page (0-based)", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info("GET /api/adhesions - page: {}, size: {}", page, size); + + List adhesions = adhesionService.getAllAdhesions(page, size); + + log.info("Récupération réussie de {} adhésions", adhesions.size()); + return Response.ok(adhesions).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des adhésions", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération des adhésions", "message", e.getMessage())) + .build(); + } + } + + /** Récupère une adhésion par son ID */ + @GET + @Path("/{id}") + @Operation( + summary = "Récupérer une adhésion par ID", + description = "Récupère les détails d'une adhésion spécifique") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Adhésion trouvée", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = AdhesionDTO.class))), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionById( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id) { + + try { + log.info("GET /api/adhesions/{}", id); + + AdhesionDTO adhesion = adhesionService.getAdhesionById(id); + + log.info("Adhésion récupérée avec succès - ID: {}", id); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Récupère une adhésion par son numéro de référence */ + @GET + @Path("/reference/{numeroReference}") + @Operation( + summary = "Récupérer une adhésion par référence", + description = "Récupère une adhésion par son numéro de référence unique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Adhésion trouvée"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionByReference( + @Parameter(description = "Numéro de référence de l'adhésion", required = true) + @PathParam("numeroReference") + @NotNull + String numeroReference) { + + try { + log.info("GET /api/adhesions/reference/{}", numeroReference); + + AdhesionDTO adhesion = adhesionService.getAdhesionByReference(numeroReference); + + log.info("Adhésion récupérée avec succès - Référence: {}", numeroReference); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée - Référence: {}", numeroReference); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "reference", numeroReference)) + .build(); + } catch (Exception e) { + log.error( + "Erreur lors de la récupération de l'adhésion - Référence: " + numeroReference, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Crée une nouvelle adhésion */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Operation( + summary = "Créer une nouvelle adhésion", + description = "Crée une nouvelle demande d'adhésion pour un membre") + @APIResponses({ + @APIResponse( + responseCode = "201", + description = "Adhésion créée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = AdhesionDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Membre ou organisation non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response createAdhesion( + @Parameter(description = "Données de l'adhésion à créer", required = true) @Valid + AdhesionDTO adhesionDTO) { + + try { + log.info( + "POST /api/adhesions - Création adhésion pour membre: {} et organisation: {}", + adhesionDTO.getMembreId(), + adhesionDTO.getOrganisationId()); + + AdhesionDTO nouvelleAdhesion = adhesionService.createAdhesion(adhesionDTO); + + log.info( + "Adhésion créée avec succès - ID: {}, Référence: {}", + nouvelleAdhesion.getId(), + nouvelleAdhesion.getNumeroReference()); + + return Response.status(Response.Status.CREATED).entity(nouvelleAdhesion).build(); + + } catch (NotFoundException e) { + log.warn("Membre ou organisation non trouvé lors de la création d'adhésion"); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Membre ou organisation non trouvé", "message", e.getMessage())) + .build(); + } catch (IllegalArgumentException e) { + log.warn("Données invalides pour la création d'adhésion: {}", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création de l'adhésion", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la création de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Met à jour une adhésion existante */ + @PUT + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}") + @Operation( + summary = "Mettre à jour une adhésion", + description = "Met à jour les données d'une adhésion existante") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Adhésion mise à jour avec succès"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response updateAdhesion( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id, + @Parameter(description = "Nouvelles données de l'adhésion", required = true) @Valid + AdhesionDTO adhesionDTO) { + + try { + log.info("PUT /api/adhesions/{}", id); + + AdhesionDTO adhesionMiseAJour = adhesionService.updateAdhesion(id, adhesionDTO); + + log.info("Adhésion mise à jour avec succès - ID: {}", id); + return Response.ok(adhesionMiseAJour).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour mise à jour - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalArgumentException e) { + log.warn( + "Données invalides pour la mise à jour d'adhésion - ID: {}, Erreur: {}", id, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la mise à jour de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la mise à jour de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Supprime une adhésion */ + @DELETE + @RolesAllowed({"ADMIN"}) + @Path("/{id}") + @Operation( + summary = "Supprimer une adhésion", + description = "Supprime (annule) une adhésion") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Adhésion supprimée avec succès"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse( + responseCode = "409", + description = "Impossible de supprimer une adhésion payée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response deleteAdhesion( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id) { + + try { + log.info("DELETE /api/adhesions/{}", id); + + adhesionService.deleteAdhesion(id); + + log.info("Adhésion supprimée avec succès - ID: {}", id); + return Response.noContent().build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour suppression - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalStateException e) { + log.warn("Impossible de supprimer l'adhésion - ID: {}, Raison: {}", id, e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(Map.of("error", "Impossible de supprimer l'adhésion", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la suppression de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la suppression de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Approuve une adhésion */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}/approuver") + @Operation( + summary = "Approuver une adhésion", + description = "Approuve une demande d'adhésion en attente") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Adhésion approuvée avec succès"), + @APIResponse(responseCode = "400", description = "L'adhésion ne peut pas être approuvée"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response approuverAdhesion( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id, + @Parameter(description = "Nom de l'utilisateur qui approuve") + @QueryParam("approuvePar") + String approuvePar) { + + try { + log.info("POST /api/adhesions/{}/approuver", id); + + AdhesionDTO adhesion = adhesionService.approuverAdhesion(id, approuvePar); + + log.info("Adhésion approuvée avec succès - ID: {}", id); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour approbation - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalStateException e) { + log.warn("Impossible d'approuver l'adhésion - ID: {}, Raison: {}", id, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Impossible d'approuver l'adhésion", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de l'approbation de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de l'approbation de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Rejette une adhésion */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}/rejeter") + @Operation( + summary = "Rejeter une adhésion", + description = "Rejette une demande d'adhésion en attente") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Adhésion rejetée avec succès"), + @APIResponse(responseCode = "400", description = "L'adhésion ne peut pas être rejetée"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response rejeterAdhesion( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id, + @Parameter(description = "Motif du rejet", required = true) @QueryParam("motifRejet") + @NotNull + String motifRejet) { + + try { + log.info("POST /api/adhesions/{}/rejeter", id); + + AdhesionDTO adhesion = adhesionService.rejeterAdhesion(id, motifRejet); + + log.info("Adhésion rejetée avec succès - ID: {}", id); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour rejet - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalStateException e) { + log.warn("Impossible de rejeter l'adhésion - ID: {}, Raison: {}", id, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Impossible de rejeter l'adhésion", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors du rejet de l'adhésion - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors du rejet de l'adhésion", "message", e.getMessage())) + .build(); + } + } + + /** Enregistre un paiement pour une adhésion */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}/paiement") + @Operation( + summary = "Enregistrer un paiement", + description = "Enregistre un paiement pour une adhésion approuvée") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Paiement enregistré avec succès"), + @APIResponse(responseCode = "400", description = "L'adhésion ne peut pas recevoir de paiement"), + @APIResponse(responseCode = "404", description = "Adhésion non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response enregistrerPaiement( + @Parameter(description = "Identifiant de l'adhésion", required = true) + @PathParam("id") + @NotNull + UUID id, + @Parameter(description = "Montant payé", required = true) @QueryParam("montantPaye") + @NotNull + BigDecimal montantPaye, + @Parameter(description = "Méthode de paiement") @QueryParam("methodePaiement") + String methodePaiement, + @Parameter(description = "Référence du paiement") @QueryParam("referencePaiement") + String referencePaiement) { + + try { + log.info("POST /api/adhesions/{}/paiement", id); + + AdhesionDTO adhesion = + adhesionService.enregistrerPaiement(id, montantPaye, methodePaiement, referencePaiement); + + log.info("Paiement enregistré avec succès pour l'adhésion - ID: {}", id); + return Response.ok(adhesion).build(); + + } catch (NotFoundException e) { + log.warn("Adhésion non trouvée pour paiement - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Adhésion non trouvée", "id", id)) + .build(); + } catch (IllegalStateException e) { + log.warn("Impossible d'enregistrer le paiement - ID: {}, Raison: {}", id, e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity( + Map.of("error", "Impossible d'enregistrer le paiement", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de l'enregistrement du paiement - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de l'enregistrement du paiement", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les adhésions d'un membre */ + @GET + @Path("/membre/{membreId}") + @Operation( + summary = "Lister les adhésions d'un membre", + description = "Récupère toutes les adhésions d'un membre spécifique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des adhésions du membre"), + @APIResponse(responseCode = "404", description = "Membre non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionsByMembre( + @Parameter(description = "Identifiant du membre", required = true) + @PathParam("membreId") + @NotNull + UUID membreId, + @Parameter(description = "Numéro de page", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info("GET /api/adhesions/membre/{} - page: {}, size: {}", membreId, page, size); + + List adhesions = adhesionService.getAdhesionsByMembre(membreId, page, size); + + log.info( + "Récupération réussie de {} adhésions pour le membre {}", adhesions.size(), membreId); + return Response.ok(adhesions).build(); + + } catch (NotFoundException e) { + log.warn("Membre non trouvé - ID: {}", membreId); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Membre non trouvé", "membreId", membreId)) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des adhésions du membre - ID: " + membreId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la récupération des adhésions", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les adhésions d'une organisation */ + @GET + @Path("/organisation/{organisationId}") + @Operation( + summary = "Lister les adhésions d'une organisation", + description = "Récupère toutes les adhésions d'une organisation spécifique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des adhésions de l'organisation"), + @APIResponse(responseCode = "404", description = "Organisation non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionsByOrganisation( + @Parameter(description = "Identifiant de l'organisation", required = true) + @PathParam("organisationId") + @NotNull + UUID organisationId, + @Parameter(description = "Numéro de page", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info( + "GET /api/adhesions/organisation/{} - page: {}, size: {}", organisationId, page, size); + + List adhesions = + adhesionService.getAdhesionsByOrganisation(organisationId, page, size); + + log.info( + "Récupération réussie de {} adhésions pour l'organisation {}", + adhesions.size(), + organisationId); + return Response.ok(adhesions).build(); + + } catch (NotFoundException e) { + log.warn("Organisation non trouvée - ID: {}", organisationId); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Organisation non trouvée", "organisationId", organisationId)) + .build(); + } catch (Exception e) { + log.error( + "Erreur lors de la récupération des adhésions de l'organisation - ID: " + organisationId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la récupération des adhésions", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les adhésions par statut */ + @GET + @Path("/statut/{statut}") + @Operation( + summary = "Lister les adhésions par statut", + description = "Récupère toutes les adhésions ayant un statut spécifique") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Liste des adhésions avec le statut spécifié"), + @APIResponse(responseCode = "400", description = "Statut invalide"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionsByStatut( + @Parameter(description = "Statut des adhésions", required = true, example = "EN_ATTENTE") + @PathParam("statut") + @NotNull + String statut, + @Parameter(description = "Numéro de page", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info("GET /api/adhesions/statut/{} - page: {}, size: {}", statut, page, size); + + List adhesions = adhesionService.getAdhesionsByStatut(statut, page, size); + + log.info("Récupération réussie de {} adhésions avec statut {}", adhesions.size(), statut); + return Response.ok(adhesions).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des adhésions par statut - Statut: " + statut, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la récupération des adhésions", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les adhésions en attente */ + @GET + @Path("/en-attente") + @Operation( + summary = "Lister les adhésions en attente", + description = "Récupère toutes les adhésions en attente d'approbation") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des adhésions en attente"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAdhesionsEnAttente( + @Parameter(description = "Numéro de page", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info("GET /api/adhesions/en-attente - page: {}, size: {}", page, size); + + List adhesions = adhesionService.getAdhesionsEnAttente(page, size); + + log.info("Récupération réussie de {} adhésions en attente", adhesions.size()); + return Response.ok(adhesions).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des adhésions en attente", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération des adhésions en attente", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les statistiques des adhésions */ + @GET + @Path("/stats") + @Operation( + summary = "Statistiques des adhésions", + description = "Récupère les statistiques globales des adhésions") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getStatistiquesAdhesions() { + try { + log.info("GET /api/adhesions/stats"); + + Map statistiques = adhesionService.getStatistiquesAdhesions(); + + log.info("Statistiques récupérées avec succès"); + return Response.ok(statistiques).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des statistiques", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération des statistiques", "message", e.getMessage())) + .build(); + } + } +} + + + diff --git a/src/main/java/dev/lions/unionflow/server/resource/AnalyticsResource.java b/src/main/java/dev/lions/unionflow/server/resource/AnalyticsResource.java new file mode 100644 index 0000000..84ae5f9 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/AnalyticsResource.java @@ -0,0 +1,345 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.analytics.AnalyticsDataDTO; +import dev.lions.unionflow.server.api.dto.analytics.DashboardWidgetDTO; +import dev.lions.unionflow.server.api.dto.analytics.KPITrendDTO; +import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse; +import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique; +import dev.lions.unionflow.server.service.AnalyticsService; +import dev.lions.unionflow.server.service.KPICalculatorService; +import io.quarkus.security.Authenticated; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** + * Ressource REST pour les analytics et métriques UnionFlow + * + *

Cette ressource expose les APIs pour accéder aux données analytics, KPI, tendances et widgets + * de tableau de bord. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-16 + */ +@Path("/api/v1/analytics") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Authenticated +@Tag(name = "Analytics", description = "APIs pour les analytics et métriques") +public class AnalyticsResource { + + private static final Logger log = Logger.getLogger(AnalyticsResource.class); + + @Inject AnalyticsService analyticsService; + + @Inject KPICalculatorService kpiCalculatorService; + + /** Calcule une métrique analytics pour une période donnée */ + @GET + @Path("/metriques/{typeMetrique}") + @RolesAllowed({"ADMIN", "MANAGER", "MEMBER"}) + @Operation( + summary = "Calculer une métrique analytics", + description = "Calcule une métrique spécifique pour une période et organisation données") + @APIResponse(responseCode = "200", description = "Métrique calculée avec succès") + @APIResponse(responseCode = "400", description = "Paramètres invalides") + @APIResponse(responseCode = "403", description = "Accès non autorisé") + public Response calculerMetrique( + @Parameter(description = "Type de métrique à calculer", required = true) + @PathParam("typeMetrique") + TypeMetrique typeMetrique, + @Parameter(description = "Période d'analyse", required = true) @QueryParam("periode") @NotNull + PeriodeAnalyse periodeAnalyse, + @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("organisationId") + UUID organisationId) { + + try { + log.infof( + "Calcul de la métrique %s pour la période %s et l'organisation %s", + typeMetrique, periodeAnalyse, organisationId); + + AnalyticsDataDTO result = + analyticsService.calculerMetrique(typeMetrique, periodeAnalyse, organisationId); + + return Response.ok(result).build(); + + } catch (Exception e) { + log.errorf(e, "Erreur lors du calcul de la métrique %s: %s", typeMetrique, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors du calcul de la métrique", "message", e.getMessage())) + .build(); + } + } + + /** Calcule les tendances d'un KPI sur une période */ + @GET + @Path("/tendances/{typeMetrique}") + @RolesAllowed({"ADMIN", "MANAGER", "MEMBER"}) + @Operation( + summary = "Calculer la tendance d'un KPI", + description = "Calcule l'évolution et les tendances d'un KPI sur une période donnée") + @APIResponse(responseCode = "200", description = "Tendance calculée avec succès") + @APIResponse(responseCode = "400", description = "Paramètres invalides") + @APIResponse(responseCode = "403", description = "Accès non autorisé") + public Response calculerTendanceKPI( + @Parameter(description = "Type de métrique pour la tendance", required = true) + @PathParam("typeMetrique") + TypeMetrique typeMetrique, + @Parameter(description = "Période d'analyse", required = true) @QueryParam("periode") @NotNull + PeriodeAnalyse periodeAnalyse, + @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("organisationId") + UUID organisationId) { + + try { + log.infof( + "Calcul de la tendance KPI %s pour la période %s et l'organisation %s", + typeMetrique, periodeAnalyse, organisationId); + + KPITrendDTO result = + analyticsService.calculerTendanceKPI(typeMetrique, periodeAnalyse, organisationId); + + return Response.ok(result).build(); + + } catch (Exception e) { + log.errorf( + e, "Erreur lors du calcul de la tendance KPI %s: %s", typeMetrique, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors du calcul de la tendance", "message", e.getMessage())) + .build(); + } + } + + /** Obtient tous les KPI pour une organisation */ + @GET + @Path("/kpis") + @RolesAllowed({"ADMIN", "MANAGER", "MEMBER"}) + @Operation( + summary = "Obtenir tous les KPI", + description = "Récupère tous les KPI calculés pour une organisation et période données") + @APIResponse(responseCode = "200", description = "KPI récupérés avec succès") + @APIResponse(responseCode = "400", description = "Paramètres invalides") + @APIResponse(responseCode = "403", description = "Accès non autorisé") + public Response obtenirTousLesKPI( + @Parameter(description = "Période d'analyse", required = true) @QueryParam("periode") @NotNull + PeriodeAnalyse periodeAnalyse, + @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("organisationId") + UUID organisationId) { + + try { + log.infof( + "Récupération de tous les KPI pour la période %s et l'organisation %s", + periodeAnalyse, organisationId); + + Map kpis = + kpiCalculatorService.calculerTousLesKPI( + organisationId, periodeAnalyse.getDateDebut(), periodeAnalyse.getDateFin()); + + return Response.ok(kpis).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des KPI: {}", e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of("error", "Erreur lors de la récupération des KPI", "message", e.getMessage())) + .build(); + } + } + + /** Calcule le KPI de performance globale */ + @GET + @Path("/performance-globale") + @RolesAllowed({"ADMIN", "MANAGER"}) + @Operation( + summary = "Calculer la performance globale", + description = "Calcule le score de performance globale de l'organisation") + @APIResponse(responseCode = "200", description = "Performance globale calculée avec succès") + @APIResponse(responseCode = "403", description = "Accès non autorisé") + public Response calculerPerformanceGlobale( + @Parameter(description = "Période d'analyse", required = true) @QueryParam("periode") @NotNull + PeriodeAnalyse periodeAnalyse, + @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("organisationId") + UUID organisationId) { + + try { + log.infof( + "Calcul de la performance globale pour la période %s et l'organisation %s", + periodeAnalyse, organisationId); + + BigDecimal performanceGlobale = + kpiCalculatorService.calculerKPIPerformanceGlobale( + organisationId, periodeAnalyse.getDateDebut(), periodeAnalyse.getDateFin()); + + return Response.ok( + Map.of( + "performanceGlobale", performanceGlobale, + "periode", periodeAnalyse, + "organisationId", organisationId, + "dateCalcul", java.time.LocalDateTime.now())) + .build(); + + } catch (Exception e) { + log.error("Erreur lors du calcul de la performance globale: {}", e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors du calcul de la performance globale", + "message", + e.getMessage())) + .build(); + } + } + + /** Obtient les évolutions des KPI par rapport à la période précédente */ + @GET + @Path("/evolutions") + @RolesAllowed({"ADMIN", "MANAGER", "MEMBER"}) + @Operation( + summary = "Obtenir les évolutions des KPI", + description = "Récupère les évolutions des KPI par rapport à la période précédente") + @APIResponse(responseCode = "200", description = "Évolutions récupérées avec succès") + @APIResponse(responseCode = "400", description = "Paramètres invalides") + @APIResponse(responseCode = "403", description = "Accès non autorisé") + public Response obtenirEvolutionsKPI( + @Parameter(description = "Période d'analyse", required = true) @QueryParam("periode") @NotNull + PeriodeAnalyse periodeAnalyse, + @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("organisationId") + UUID organisationId) { + + try { + log.infof( + "Récupération des évolutions KPI pour la période %s et l'organisation %s", + periodeAnalyse, organisationId); + + Map evolutions = + kpiCalculatorService.calculerEvolutionsKPI( + organisationId, periodeAnalyse.getDateDebut(), periodeAnalyse.getDateFin()); + + return Response.ok(evolutions).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des évolutions KPI: {}", e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération des évolutions", + "message", + e.getMessage())) + .build(); + } + } + + /** Obtient les widgets du tableau de bord pour un utilisateur */ + @GET + @Path("/dashboard/widgets") + @RolesAllowed({"ADMIN", "MANAGER", "MEMBER"}) + @Operation( + summary = "Obtenir les widgets du tableau de bord", + description = "Récupère tous les widgets configurés pour le tableau de bord de l'utilisateur") + @APIResponse(responseCode = "200", description = "Widgets récupérés avec succès") + @APIResponse(responseCode = "403", description = "Accès non autorisé") + public Response obtenirWidgetsTableauBord( + @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("organisationId") + UUID organisationId, + @Parameter(description = "ID de l'utilisateur", required = true) + @QueryParam("utilisateurId") + @NotNull + UUID utilisateurId) { + + try { + log.infof( + "Récupération des widgets du tableau de bord pour l'organisation %s et l'utilisateur %s", + organisationId, utilisateurId); + + List widgets = + analyticsService.obtenirMetriquesTableauBord(organisationId, utilisateurId); + + return Response.ok(widgets).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des widgets: {}", e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la récupération des widgets", "message", e.getMessage())) + .build(); + } + } + + /** Obtient les types de métriques disponibles */ + @GET + @Path("/types-metriques") + @RolesAllowed({"ADMIN", "MANAGER", "MEMBER"}) + @Operation( + summary = "Obtenir les types de métriques disponibles", + description = "Récupère la liste de tous les types de métriques disponibles") + @APIResponse(responseCode = "200", description = "Types de métriques récupérés avec succès") + public Response obtenirTypesMetriques() { + try { + log.info("Récupération des types de métriques disponibles"); + + TypeMetrique[] typesMetriques = TypeMetrique.values(); + + return Response.ok(Map.of("typesMetriques", typesMetriques, "total", typesMetriques.length)) + .build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des types de métriques: {}", e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération des types de métriques", + "message", + e.getMessage())) + .build(); + } + } + + /** Obtient les périodes d'analyse disponibles */ + @GET + @Path("/periodes-analyse") + @RolesAllowed({"ADMIN", "MANAGER", "MEMBER"}) + @Operation( + summary = "Obtenir les périodes d'analyse disponibles", + description = "Récupère la liste de toutes les périodes d'analyse disponibles") + @APIResponse(responseCode = "200", description = "Périodes d'analyse récupérées avec succès") + public Response obtenirPeriodesAnalyse() { + try { + log.info("Récupération des périodes d'analyse disponibles"); + + PeriodeAnalyse[] periodesAnalyse = PeriodeAnalyse.values(); + + return Response.ok( + Map.of("periodesAnalyse", periodesAnalyse, "total", periodesAnalyse.length)) + .build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des périodes d'analyse: {}", e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération des périodes d'analyse", + "message", + e.getMessage())) + .build(); + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/AuditResource.java b/src/main/java/dev/lions/unionflow/server/resource/AuditResource.java new file mode 100644 index 0000000..aa792b0 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/AuditResource.java @@ -0,0 +1,112 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.admin.AuditLogDTO; +import dev.lions.unionflow.server.service.AuditService; +import jakarta.inject.Inject; +import jakarta.annotation.security.RolesAllowed; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.time.LocalDateTime; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * Resource REST pour la gestion des logs d'audit + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@Path("/api/audit") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Audit", description = "Gestion des logs d'audit") +@Slf4j +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class AuditResource { + + @Inject + AuditService auditService; + + @GET + @Operation(summary = "Liste tous les logs d'audit", description = "Récupère tous les logs avec pagination") + public Response listerTous( + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("50") int size, + @QueryParam("sortBy") @DefaultValue("dateHeure") String sortBy, + @QueryParam("sortOrder") @DefaultValue("desc") String sortOrder) { + + try { + Map result = auditService.listerTous(page, size, sortBy, sortOrder); + return Response.ok(result).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des logs d'audit", e); + return Response.serverError() + .entity(Map.of("error", "Erreur lors de la récupération des logs: " + e.getMessage())) + .build(); + } + } + + @POST + @Path("/rechercher") + @Operation(summary = "Recherche des logs avec filtres", description = "Recherche avancée avec filtres multiples") + public Response rechercher( + @QueryParam("dateDebut") String dateDebutStr, + @QueryParam("dateFin") String dateFinStr, + @QueryParam("typeAction") String typeAction, + @QueryParam("severite") String severite, + @QueryParam("utilisateur") String utilisateur, + @QueryParam("module") String module, + @QueryParam("ipAddress") String ipAddress, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("50") int size) { + + try { + LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null; + LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null; + + Map result = auditService.rechercher( + dateDebut, dateFin, typeAction, severite, utilisateur, module, ipAddress, page, size); + return Response.ok(result).build(); + } catch (Exception e) { + log.error("Erreur lors de la recherche des logs d'audit", e); + return Response.serverError() + .entity(Map.of("error", "Erreur lors de la recherche: " + e.getMessage())) + .build(); + } + } + + @POST + @Operation(summary = "Enregistre un nouveau log d'audit", description = "Crée une nouvelle entrée dans le journal d'audit") + public Response enregistrerLog(@Valid AuditLogDTO dto) { + try { + AuditLogDTO result = auditService.enregistrerLog(dto); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (Exception e) { + log.error("Erreur lors de l'enregistrement du log d'audit", e); + return Response.serverError() + .entity(Map.of("error", "Erreur lors de l'enregistrement: " + e.getMessage())) + .build(); + } + } + + @GET + @Path("/statistiques") + @Operation(summary = "Récupère les statistiques d'audit", description = "Retourne les statistiques globales des logs") + public Response getStatistiques() { + try { + Map stats = auditService.getStatistiques(); + return Response.ok(stats).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des statistiques", e); + return Response.serverError() + .entity(Map.of("error", "Erreur lors de la récupération des statistiques: " + e.getMessage())) + .build(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/resource/ComptabiliteResource.java b/src/main/java/dev/lions/unionflow/server/resource/ComptabiliteResource.java new file mode 100644 index 0000000..512267e --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/ComptabiliteResource.java @@ -0,0 +1,278 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.comptabilite.*; +import dev.lions.unionflow.server.service.ComptabiliteService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Resource REST pour la gestion comptable + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Path("/api/comptabilite") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class ComptabiliteResource { + + private static final Logger LOG = Logger.getLogger(ComptabiliteResource.class); + + @Inject ComptabiliteService comptabiliteService; + + // ======================================== + // COMPTES COMPTABLES + // ======================================== + + /** + * Crée un nouveau compte comptable + * + * @param compteDTO DTO du compte à créer + * @return Compte créé + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/comptes") + public Response creerCompteComptable(@Valid CompteComptableDTO compteDTO) { + try { + CompteComptableDTO result = comptabiliteService.creerCompteComptable(compteDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création du compte comptable"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création du compte comptable: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un compte comptable par son ID + * + * @param id ID du compte + * @return Compte comptable + */ + @GET + @Path("/comptes/{id}") + public Response trouverCompteParId(@PathParam("id") UUID id) { + try { + CompteComptableDTO result = comptabiliteService.trouverCompteParId(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Compte comptable non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche du compte comptable"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du compte comptable: " + e.getMessage())) + .build(); + } + } + + /** + * Liste tous les comptes comptables actifs + * + * @return Liste des comptes + */ + @GET + @Path("/comptes") + public Response listerTousLesComptes() { + try { + List result = comptabiliteService.listerTousLesComptes(); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des comptes comptables"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des comptes comptables: " + e.getMessage())) + .build(); + } + } + + // ======================================== + // JOURNAUX COMPTABLES + // ======================================== + + /** + * Crée un nouveau journal comptable + * + * @param journalDTO DTO du journal à créer + * @return Journal créé + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/journaux") + public Response creerJournalComptable(@Valid JournalComptableDTO journalDTO) { + try { + JournalComptableDTO result = comptabiliteService.creerJournalComptable(journalDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création du journal comptable"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création du journal comptable: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un journal comptable par son ID + * + * @param id ID du journal + * @return Journal comptable + */ + @GET + @Path("/journaux/{id}") + public Response trouverJournalParId(@PathParam("id") UUID id) { + try { + JournalComptableDTO result = comptabiliteService.trouverJournalParId(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Journal comptable non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche du journal comptable"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du journal comptable: " + e.getMessage())) + .build(); + } + } + + /** + * Liste tous les journaux comptables actifs + * + * @return Liste des journaux + */ + @GET + @Path("/journaux") + public Response listerTousLesJournaux() { + try { + List result = comptabiliteService.listerTousLesJournaux(); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des journaux comptables"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des journaux comptables: " + e.getMessage())) + .build(); + } + } + + // ======================================== + // ÉCRITURES COMPTABLES + // ======================================== + + /** + * Crée une nouvelle écriture comptable + * + * @param ecritureDTO DTO de l'écriture à créer + * @return Écriture créée + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/ecritures") + public Response creerEcritureComptable(@Valid EcritureComptableDTO ecritureDTO) { + try { + EcritureComptableDTO result = comptabiliteService.creerEcritureComptable(ecritureDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création de l'écriture comptable"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création de l'écriture comptable: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve une écriture comptable par son ID + * + * @param id ID de l'écriture + * @return Écriture comptable + */ + @GET + @Path("/ecritures/{id}") + public Response trouverEcritureParId(@PathParam("id") UUID id) { + try { + EcritureComptableDTO result = comptabiliteService.trouverEcritureParId(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Écriture comptable non trouvée")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche de l'écriture comptable"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche de l'écriture comptable: " + e.getMessage())) + .build(); + } + } + + /** + * Liste les écritures d'un journal + * + * @param journalId ID du journal + * @return Liste des écritures + */ + @GET + @Path("/ecritures/journal/{journalId}") + public Response listerEcrituresParJournal(@PathParam("journalId") UUID journalId) { + try { + List result = comptabiliteService.listerEcrituresParJournal(journalId); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des écritures"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des écritures: " + e.getMessage())) + .build(); + } + } + + /** + * Liste les écritures d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des écritures + */ + @GET + @Path("/ecritures/organisation/{organisationId}") + public Response listerEcrituresParOrganisation(@PathParam("organisationId") UUID organisationId) { + try { + List result = comptabiliteService.listerEcrituresParOrganisation(organisationId); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des écritures"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des écritures: " + e.getMessage())) + .build(); + } + } + + /** Classe interne pour les réponses d'erreur */ + public static class ErrorResponse { + public String error; + + public ErrorResponse(String error) { + this.error = error; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/resource/CotisationResource.java b/src/main/java/dev/lions/unionflow/server/resource/CotisationResource.java new file mode 100644 index 0000000..0ec502d --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/CotisationResource.java @@ -0,0 +1,674 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.finance.CotisationDTO; +import dev.lions.unionflow.server.service.CotisationService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** + * Resource REST pour la gestion des cotisations Expose les endpoints API pour les opérations CRUD + * sur les cotisations + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@Path("/api/cotisations") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Cotisations", description = "Gestion des cotisations des membres") +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +@Slf4j +public class CotisationResource { + + @Inject CotisationService cotisationService; + + /** Endpoint public pour les cotisations (test) */ + @GET + @Path("/public") + @Operation( + summary = "Cotisations publiques", + description = "Liste des cotisations sans authentification") + @APIResponse(responseCode = "200", description = "Liste des cotisations") + public Response getCotisationsPublic( + @QueryParam("page") @DefaultValue("0") @Min(0) int page, + @QueryParam("size") @DefaultValue("20") @Min(1) int size) { + + try { + log.info("GET /api/cotisations/public - page: {}, size: {}", page, size); + + // Récupérer les cotisations depuis la base de données + List cotisationsDTO = cotisationService.getAllCotisations(page, size); + + // Convertir en format pour l'application mobile + List> cotisations = cotisationsDTO.stream() + .map(c -> { + Map map = new java.util.HashMap<>(); + map.put("id", c.getId() != null ? c.getId().toString() : ""); + map.put("nom", c.getDescription() != null ? c.getDescription() : "Cotisation"); + map.put("description", c.getDescription() != null ? c.getDescription() : ""); + map.put("montant", c.getMontantDu() != null ? c.getMontantDu().doubleValue() : 0.0); + map.put("devise", c.getCodeDevise() != null ? c.getCodeDevise() : "XOF"); + map.put("dateEcheance", c.getDateEcheance() != null ? c.getDateEcheance().toString() : ""); + map.put("statut", c.getStatut() != null ? c.getStatut() : "EN_ATTENTE"); + map.put("type", c.getTypeCotisation() != null ? c.getTypeCotisation() : "MENSUELLE"); + return map; + }) + .collect(Collectors.toList()); + + long totalElements = cotisationService.getStatistiquesCotisations().get("totalCotisations") != null + ? ((Number) cotisationService.getStatistiquesCotisations().get("totalCotisations")).longValue() + : cotisations.size(); + int totalPages = (int) Math.ceil((double) totalElements / size); + + Map response = + Map.of( + "content", cotisations, + "totalElements", totalElements, + "totalPages", totalPages, + "size", size, + "number", page); + + return Response.ok(response).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des cotisations publiques", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération des cotisations")) + .build(); + } + } + + /** Récupère toutes les cotisations avec pagination */ + @GET + @Operation( + summary = "Lister toutes les cotisations", + description = "Récupère la liste paginée de toutes les cotisations") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Liste des cotisations récupérée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = CotisationDTO.class))), + @APIResponse(responseCode = "400", description = "Paramètres de pagination invalides"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getAllCotisations( + @Parameter(description = "Numéro de page (0-based)", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info("GET /api/cotisations - page: {}, size: {}", page, size); + + List cotisations = cotisationService.getAllCotisations(page, size); + + log.info("Récupération réussie de {} cotisations", cotisations.size()); + return Response.ok(cotisations).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des cotisations", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération des cotisations", + "message", + e.getMessage())) + .build(); + } + } + + /** Récupère une cotisation par son ID */ + @GET + @Path("/{id}") + @Operation( + summary = "Récupérer une cotisation par ID", + description = "Récupère les détails d'une cotisation spécifique") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Cotisation trouvée", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = CotisationDTO.class))), + @APIResponse(responseCode = "404", description = "Cotisation non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getCotisationById( + @Parameter(description = "Identifiant de la cotisation", required = true) + @PathParam("id") + @NotNull + UUID id) { + + try { + log.info("GET /api/cotisations/{}", id); + + CotisationDTO cotisation = cotisationService.getCotisationById(id); + + log.info("Cotisation récupérée avec succès - ID: {}", id); + return Response.ok(cotisation).build(); + + } catch (NotFoundException e) { + log.warn("Cotisation non trouvée - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Cotisation non trouvée", "id", id)) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération de la cotisation - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération de la cotisation", + "message", + e.getMessage())) + .build(); + } + } + + /** Récupère une cotisation par son numéro de référence */ + @GET + @Path("/reference/{numeroReference}") + @Operation( + summary = "Récupérer une cotisation par référence", + description = "Récupère une cotisation par son numéro de référence unique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Cotisation trouvée"), + @APIResponse(responseCode = "404", description = "Cotisation non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getCotisationByReference( + @Parameter(description = "Numéro de référence de la cotisation", required = true) + @PathParam("numeroReference") + @NotNull + String numeroReference) { + + try { + log.info("GET /api/cotisations/reference/{}", numeroReference); + + CotisationDTO cotisation = cotisationService.getCotisationByReference(numeroReference); + + log.info("Cotisation récupérée avec succès - Référence: {}", numeroReference); + return Response.ok(cotisation).build(); + + } catch (NotFoundException e) { + log.warn("Cotisation non trouvée - Référence: {}", numeroReference); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Cotisation non trouvée", "reference", numeroReference)) + .build(); + } catch (Exception e) { + log.error( + "Erreur lors de la récupération de la cotisation - Référence: " + numeroReference, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération de la cotisation", + "message", + e.getMessage())) + .build(); + } + } + + /** Crée une nouvelle cotisation */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Operation( + summary = "Créer une nouvelle cotisation", + description = "Crée une nouvelle cotisation pour un membre") + @APIResponses({ + @APIResponse( + responseCode = "201", + description = "Cotisation créée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = CotisationDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Membre non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response createCotisation( + @Parameter(description = "Données de la cotisation à créer", required = true) @Valid + CotisationDTO cotisationDTO) { + + try { + log.info( + "POST /api/cotisations - Création cotisation pour membre: {}", + cotisationDTO.getMembreId()); + + CotisationDTO nouvelleCotisation = cotisationService.createCotisation(cotisationDTO); + + log.info( + "Cotisation créée avec succès - ID: {}, Référence: {}", + nouvelleCotisation.getId(), + nouvelleCotisation.getNumeroReference()); + + return Response.status(Response.Status.CREATED).entity(nouvelleCotisation).build(); + + } catch (NotFoundException e) { + log.warn( + "Membre non trouvé lors de la création de cotisation: {}", cotisationDTO.getMembreId()); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Membre non trouvé", "membreId", cotisationDTO.getMembreId())) + .build(); + } catch (IllegalArgumentException e) { + log.warn("Données invalides pour la création de cotisation: {}", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création de la cotisation", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la création de la cotisation", + "message", + e.getMessage())) + .build(); + } + } + + /** Met à jour une cotisation existante */ + @PUT + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}") + @Operation( + summary = "Mettre à jour une cotisation", + description = "Met à jour les données d'une cotisation existante") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Cotisation mise à jour avec succès"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Cotisation non trouvée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response updateCotisation( + @Parameter(description = "Identifiant de la cotisation", required = true) + @PathParam("id") + @NotNull + UUID id, + @Parameter(description = "Nouvelles données de la cotisation", required = true) @Valid + CotisationDTO cotisationDTO) { + + try { + log.info("PUT /api/cotisations/{}", id); + + CotisationDTO cotisationMiseAJour = cotisationService.updateCotisation(id, cotisationDTO); + + log.info("Cotisation mise à jour avec succès - ID: {}", id); + return Response.ok(cotisationMiseAJour).build(); + + } catch (NotFoundException e) { + log.warn("Cotisation non trouvée pour mise à jour - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Cotisation non trouvée", "id", id)) + .build(); + } catch (IllegalArgumentException e) { + log.warn( + "Données invalides pour la mise à jour de cotisation - ID: {}, Erreur: {}", + id, + e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Données invalides", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la mise à jour de la cotisation - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la mise à jour de la cotisation", + "message", + e.getMessage())) + .build(); + } + } + + /** Supprime une cotisation */ + @DELETE + @RolesAllowed({"ADMIN"}) + @Path("/{id}") + @Operation( + summary = "Supprimer une cotisation", + description = "Supprime (désactive) une cotisation") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Cotisation supprimée avec succès"), + @APIResponse(responseCode = "404", description = "Cotisation non trouvée"), + @APIResponse( + responseCode = "409", + description = "Impossible de supprimer une cotisation payée"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response deleteCotisation( + @Parameter(description = "Identifiant de la cotisation", required = true) + @PathParam("id") + @NotNull + UUID id) { + + try { + log.info("DELETE /api/cotisations/{}", id); + + cotisationService.deleteCotisation(id); + + log.info("Cotisation supprimée avec succès - ID: {}", id); + return Response.noContent().build(); + + } catch (NotFoundException e) { + log.warn("Cotisation non trouvée pour suppression - ID: {}", id); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Cotisation non trouvée", "id", id)) + .build(); + } catch (IllegalStateException e) { + log.warn("Impossible de supprimer la cotisation - ID: {}, Raison: {}", id, e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity( + Map.of("error", "Impossible de supprimer la cotisation", "message", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la suppression de la cotisation - ID: " + id, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la suppression de la cotisation", + "message", + e.getMessage())) + .build(); + } + } + + /** Récupère les cotisations d'un membre */ + @GET + @Path("/membre/{membreId}") + @Operation( + summary = "Lister les cotisations d'un membre", + description = "Récupère toutes les cotisations d'un membre spécifique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des cotisations du membre"), + @APIResponse(responseCode = "404", description = "Membre non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getCotisationsByMembre( + @Parameter(description = "Identifiant du membre", required = true) + @PathParam("membreId") + @NotNull + UUID membreId, + @Parameter(description = "Numéro de page", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info("GET /api/cotisations/membre/{} - page: {}, size: {}", membreId, page, size); + + List cotisations = + cotisationService.getCotisationsByMembre(membreId, page, size); + + log.info( + "Récupération réussie de {} cotisations pour le membre {}", cotisations.size(), membreId); + return Response.ok(cotisations).build(); + + } catch (NotFoundException e) { + log.warn("Membre non trouvé - ID: {}", membreId); + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Membre non trouvé", "membreId", membreId)) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des cotisations du membre - ID: " + membreId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération des cotisations", + "message", + e.getMessage())) + .build(); + } + } + + /** Récupère les cotisations par statut */ + @GET + @Path("/statut/{statut}") + @Operation( + summary = "Lister les cotisations par statut", + description = "Récupère toutes les cotisations ayant un statut spécifique") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Liste des cotisations avec le statut spécifié"), + @APIResponse(responseCode = "400", description = "Statut invalide"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getCotisationsByStatut( + @Parameter(description = "Statut des cotisations", required = true, example = "EN_ATTENTE") + @PathParam("statut") + @NotNull + String statut, + @Parameter(description = "Numéro de page", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info("GET /api/cotisations/statut/{} - page: {}, size: {}", statut, page, size); + + List cotisations = + cotisationService.getCotisationsByStatut(statut, page, size); + + log.info("Récupération réussie de {} cotisations avec statut {}", cotisations.size(), statut); + return Response.ok(cotisations).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des cotisations par statut - Statut: " + statut, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération des cotisations", + "message", + e.getMessage())) + .build(); + } + } + + /** Récupère les cotisations en retard */ + @GET + @Path("/en-retard") + @Operation( + summary = "Lister les cotisations en retard", + description = "Récupère toutes les cotisations dont la date d'échéance est dépassée") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des cotisations en retard"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getCotisationsEnRetard( + @Parameter(description = "Numéro de page", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info("GET /api/cotisations/en-retard - page: {}, size: {}", page, size); + + List cotisations = cotisationService.getCotisationsEnRetard(page, size); + + log.info("Récupération réussie de {} cotisations en retard", cotisations.size()); + return Response.ok(cotisations).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des cotisations en retard", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération des cotisations en retard", + "message", + e.getMessage())) + .build(); + } + } + + /** Recherche avancée de cotisations */ + @GET + @Path("/recherche") + @Operation( + summary = "Recherche avancée de cotisations", + description = "Recherche de cotisations avec filtres multiples") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Résultats de la recherche"), + @APIResponse(responseCode = "400", description = "Paramètres de recherche invalides"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response rechercherCotisations( + @Parameter(description = "Identifiant du membre") @QueryParam("membreId") UUID membreId, + @Parameter(description = "Statut de la cotisation") @QueryParam("statut") String statut, + @Parameter(description = "Type de cotisation") @QueryParam("typeCotisation") + String typeCotisation, + @Parameter(description = "Année") @QueryParam("annee") Integer annee, + @Parameter(description = "Mois") @QueryParam("mois") Integer mois, + @Parameter(description = "Numéro de page", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size) { + + try { + log.info( + "GET /api/cotisations/recherche - Filtres: membreId={}, statut={}, type={}, annee={}," + + " mois={}", + membreId, + statut, + typeCotisation, + annee, + mois); + + List cotisations = + cotisationService.rechercherCotisations( + membreId, statut, typeCotisation, annee, mois, page, size); + + log.info("Recherche réussie - {} cotisations trouvées", cotisations.size()); + return Response.ok(cotisations).build(); + + } catch (Exception e) { + log.error("Erreur lors de la recherche de cotisations", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", "Erreur lors de la recherche de cotisations", "message", e.getMessage())) + .build(); + } + } + + /** Récupère les statistiques des cotisations */ + @GET + @Path("/stats") + @Operation( + summary = "Statistiques des cotisations", + description = "Récupère les statistiques globales des cotisations") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + public Response getStatistiquesCotisations() { + try { + log.info("GET /api/cotisations/stats"); + + Map statistiques = cotisationService.getStatistiquesCotisations(); + + log.info("Statistiques récupérées avec succès"); + return Response.ok(statistiques).build(); + + } catch (Exception e) { + log.error("Erreur lors de la récupération des statistiques", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity( + Map.of( + "error", + "Erreur lors de la récupération des statistiques", + "message", + e.getMessage())) + .build(); + } + } + + /** + * Envoie des rappels de cotisations groupés à plusieurs membres (WOU/DRY) + * + * @param membreIds Liste des IDs des membres destinataires + * @return Nombre de rappels envoyés + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/rappels/groupes") + @Consumes(MediaType.APPLICATION_JSON) + @Operation(summary = "Envoyer des rappels de cotisations groupés") + @APIResponse(responseCode = "200", description = "Rappels envoyés avec succès") + public Response envoyerRappelsGroupes(List membreIds) { + try { + int rappelsEnvoyes = cotisationService.envoyerRappelsCotisationsGroupes(membreIds); + return Response.ok(Map.of("rappelsEnvoyes", rappelsEnvoyes)).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de l'envoi des rappels groupés", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de l'envoi des rappels: " + e.getMessage())) + .build(); + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/DashboardResource.java b/src/main/java/dev/lions/unionflow/server/resource/DashboardResource.java new file mode 100644 index 0000000..6668d17 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/DashboardResource.java @@ -0,0 +1,251 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.dashboard.DashboardDataDTO; +import dev.lions.unionflow.server.api.dto.dashboard.DashboardStatsDTO; +import dev.lions.unionflow.server.api.dto.dashboard.RecentActivityDTO; +import dev.lions.unionflow.server.api.dto.dashboard.UpcomingEventDTO; +import dev.lions.unionflow.server.api.service.dashboard.DashboardService; +import jakarta.inject.Inject; +import jakarta.annotation.security.RolesAllowed; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Resource REST pour les APIs du dashboard + * + *

Cette ressource expose les endpoints pour récupérer les données du dashboard, + * incluant les statistiques, activités récentes et événements à venir. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@Path("/api/v1/dashboard") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Dashboard", description = "APIs pour la gestion du dashboard") +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class DashboardResource { + + private static final Logger LOG = Logger.getLogger(DashboardResource.class); + + @Inject + DashboardService dashboardService; + + /** + * Récupère toutes les données du dashboard + */ + @GET + @Path("/data") + @Operation( + summary = "Récupérer toutes les données du dashboard", + description = "Retourne les statistiques, activités récentes et événements à venir" + ) + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Données récupérées avec succès"), + @APIResponse(responseCode = "400", description = "Paramètres invalides"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + public Response getDashboardData( + @Parameter(description = "ID de l'organisation", required = true) + @QueryParam("organizationId") @NotNull String organizationId, + @Parameter(description = "ID de l'utilisateur", required = true) + @QueryParam("userId") @NotNull String userId) { + + LOG.infof("GET /api/v1/dashboard/data - org: %s, user: %s", organizationId, userId); + + try { + DashboardDataDTO dashboardData = dashboardService.getDashboardData(organizationId, userId); + return Response.ok(dashboardData).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la récupération des données dashboard"); + return Response.serverError().entity(Map.of("error", "Erreur serveur")).build(); + } + } + + /** + * Récupère uniquement les statistiques du dashboard + */ + @GET + @Path("/stats") + @Operation( + summary = "Récupérer les statistiques du dashboard", + description = "Retourne uniquement les statistiques principales" + ) + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès"), + @APIResponse(responseCode = "400", description = "Paramètres invalides"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + public Response getDashboardStats( + @Parameter(description = "ID de l'organisation", required = true) + @QueryParam("organizationId") @NotNull String organizationId, + @Parameter(description = "ID de l'utilisateur", required = true) + @QueryParam("userId") @NotNull String userId) { + + LOG.infof("GET /api/v1/dashboard/stats - org: %s, user: %s", organizationId, userId); + + try { + DashboardStatsDTO stats = dashboardService.getDashboardStats(organizationId, userId); + return Response.ok(stats).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la récupération des statistiques dashboard"); + return Response.serverError().entity(Map.of("error", "Erreur serveur")).build(); + } + } + + /** + * Récupère les activités récentes + */ + @GET + @Path("/activities") + @Operation( + summary = "Récupérer les activités récentes", + description = "Retourne la liste des activités récentes avec pagination" + ) + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Activités récupérées avec succès"), + @APIResponse(responseCode = "400", description = "Paramètres invalides"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + public Response getRecentActivities( + @Parameter(description = "ID de l'organisation", required = true) + @QueryParam("organizationId") @NotNull String organizationId, + @Parameter(description = "ID de l'utilisateur", required = true) + @QueryParam("userId") @NotNull String userId, + @Parameter(description = "Nombre maximum d'activités à retourner", required = false) + @QueryParam("limit") @DefaultValue("10") int limit) { + + LOG.infof("GET /api/v1/dashboard/activities - org: %s, user: %s, limit: %d", + organizationId, userId, limit); + + try { + List activities = dashboardService.getRecentActivities( + organizationId, userId, limit); + + Map response = new HashMap<>(); + response.put("activities", activities); + response.put("total", activities.size()); + response.put("limit", limit); + + return Response.ok(response).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la récupération des activités récentes"); + return Response.serverError().entity(Map.of("error", "Erreur serveur")).build(); + } + } + + /** + * Récupère les événements à venir + */ + @GET + @Path("/events/upcoming") + @Operation( + summary = "Récupérer les événements à venir", + description = "Retourne la liste des événements à venir avec pagination" + ) + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Événements récupérés avec succès"), + @APIResponse(responseCode = "400", description = "Paramètres invalides"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + public Response getUpcomingEvents( + @Parameter(description = "ID de l'organisation", required = true) + @QueryParam("organizationId") @NotNull String organizationId, + @Parameter(description = "ID de l'utilisateur", required = true) + @QueryParam("userId") @NotNull String userId, + @Parameter(description = "Nombre maximum d'événements à retourner", required = false) + @QueryParam("limit") @DefaultValue("5") int limit) { + + LOG.infof("GET /api/v1/dashboard/events/upcoming - org: %s, user: %s, limit: %d", + organizationId, userId, limit); + + try { + List events = dashboardService.getUpcomingEvents( + organizationId, userId, limit); + + Map response = new HashMap<>(); + response.put("events", events); + response.put("total", events.size()); + response.put("limit", limit); + + return Response.ok(response).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la récupération des événements à venir"); + return Response.serverError().entity(Map.of("error", "Erreur serveur")).build(); + } + } + + /** + * Endpoint de santé pour vérifier le statut du dashboard + */ + @GET + @Path("/health") + @Operation( + summary = "Vérifier la santé du service dashboard", + description = "Retourne le statut de santé du service dashboard" + ) + @APIResponse(responseCode = "200", description = "Service en bonne santé") + public Response healthCheck() { + LOG.debug("GET /api/v1/dashboard/health"); + + Map health = new HashMap<>(); + health.put("status", "UP"); + health.put("service", "dashboard"); + health.put("timestamp", System.currentTimeMillis()); + health.put("version", "1.0.0"); + + return Response.ok(health).build(); + } + + /** + * Endpoint pour rafraîchir les données du dashboard + */ + @POST + @Path("/refresh") + @Operation( + summary = "Rafraîchir les données du dashboard", + description = "Force la mise à jour des données du dashboard" + ) + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Données rafraîchies avec succès"), + @APIResponse(responseCode = "400", description = "Paramètres invalides"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + public Response refreshDashboard( + @Parameter(description = "ID de l'organisation", required = true) + @QueryParam("organizationId") @NotNull String organizationId, + @Parameter(description = "ID de l'utilisateur", required = true) + @QueryParam("userId") @NotNull String userId) { + + LOG.infof("POST /api/v1/dashboard/refresh - org: %s, user: %s", organizationId, userId); + + try { + // Simuler un rafraîchissement (dans un vrai système, cela pourrait vider le cache) + DashboardDataDTO dashboardData = dashboardService.getDashboardData(organizationId, userId); + + Map response = new HashMap<>(); + response.put("status", "refreshed"); + response.put("timestamp", System.currentTimeMillis()); + response.put("data", dashboardData); + + return Response.ok(response).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du rafraîchissement du dashboard"); + return Response.serverError().entity(Map.of("error", "Erreur serveur")).build(); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/resource/DocumentResource.java b/src/main/java/dev/lions/unionflow/server/resource/DocumentResource.java new file mode 100644 index 0000000..67df12a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/DocumentResource.java @@ -0,0 +1,158 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.document.DocumentDTO; +import dev.lions.unionflow.server.api.dto.document.PieceJointeDTO; +import dev.lions.unionflow.server.service.DocumentService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Resource REST pour la gestion documentaire + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Path("/api/documents") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class DocumentResource { + + private static final Logger LOG = Logger.getLogger(DocumentResource.class); + + @Inject DocumentService documentService; + + /** + * Crée un nouveau document + * + * @param documentDTO DTO du document à créer + * @return Document créé + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + public Response creerDocument(@Valid DocumentDTO documentDTO) { + try { + DocumentDTO result = documentService.creerDocument(documentDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création du document"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création du document: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un document par son ID + * + * @param id ID du document + * @return Document + */ + @GET + @Path("/{id}") + public Response trouverParId(@PathParam("id") UUID id) { + try { + DocumentDTO result = documentService.trouverParId(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Document non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche du document"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du document: " + e.getMessage())) + .build(); + } + } + + /** + * Enregistre un téléchargement de document + * + * @param id ID du document + * @return Succès + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}/telechargement") + public Response enregistrerTelechargement(@PathParam("id") UUID id) { + try { + documentService.enregistrerTelechargement(id); + return Response.ok().build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Document non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'enregistrement du téléchargement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity( + new ErrorResponse( + "Erreur lors de l'enregistrement du téléchargement: " + e.getMessage())) + .build(); + } + } + + /** + * Crée une pièce jointe + * + * @param pieceJointeDTO DTO de la pièce jointe à créer + * @return Pièce jointe créée + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/pieces-jointes") + public Response creerPieceJointe(@Valid PieceJointeDTO pieceJointeDTO) { + try { + PieceJointeDTO result = documentService.creerPieceJointe(pieceJointeDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création de la pièce jointe"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création de la pièce jointe: " + e.getMessage())) + .build(); + } + } + + /** + * Liste toutes les pièces jointes d'un document + * + * @param documentId ID du document + * @return Liste des pièces jointes + */ + @GET + @Path("/{documentId}/pieces-jointes") + public Response listerPiecesJointesParDocument(@PathParam("documentId") UUID documentId) { + try { + List result = documentService.listerPiecesJointesParDocument(documentId); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des pièces jointes"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des pièces jointes: " + e.getMessage())) + .build(); + } + } + + /** Classe interne pour les réponses d'erreur */ + public static class ErrorResponse { + public String error; + + public ErrorResponse(String error) { + this.error = error; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/resource/EvenementResource.java b/src/main/java/dev/lions/unionflow/server/resource/EvenementResource.java new file mode 100644 index 0000000..81d9a8b --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/EvenementResource.java @@ -0,0 +1,452 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.dto.EvenementMobileDTO; +import dev.lions.unionflow.server.entity.Evenement; +import dev.lions.unionflow.server.entity.Evenement.StatutEvenement; +import dev.lions.unionflow.server.entity.Evenement.TypeEvenement; +import dev.lions.unionflow.server.service.EvenementService; +import java.util.stream.Collectors; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** + * Resource REST pour la gestion des événements + * + *

Fournit les endpoints API pour les opérations CRUD sur les événements, optimisé pour + * l'intégration avec l'application mobile UnionFlow. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@Path("/api/evenements") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Événements", description = "Gestion des événements de l'union") +public class EvenementResource { + + private static final Logger LOG = Logger.getLogger(EvenementResource.class); + + @Inject EvenementService evenementService; + + /** Endpoint de test public pour vérifier la connectivité */ + @GET + @Path("/test") + @Operation( + summary = "Test de connectivité", + description = "Endpoint public pour tester la connectivité") + @APIResponse(responseCode = "200", description = "Test réussi") + public Response testConnectivity() { + LOG.info("Test de connectivité appelé depuis l'application mobile"); + return Response.ok( + Map.of( + "status", "success", + "message", "Serveur UnionFlow opérationnel", + "timestamp", System.currentTimeMillis(), + "version", "1.0.0")) + .build(); + } + + /** Endpoint de debug pour vérifier le chargement des données */ + @GET + @Path("/count") + @Operation(summary = "Compter les événements", description = "Compte le nombre d'événements dans la base") + @APIResponse(responseCode = "200", description = "Nombre d'événements") + public Response countEvenements() { + try { + long count = evenementService.countEvenements(); + return Response.ok(Map.of("count", count, "status", "success")).build(); + } catch (Exception e) { + LOG.errorf("Erreur count: %s", e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", e.getMessage())) + .build(); + } + } + + /** Liste tous les événements actifs avec pagination */ + @GET + @Operation( + summary = "Lister tous les événements actifs", + description = "Récupère la liste paginée des événements actifs") + @APIResponse(responseCode = "200", description = "Liste des événements actifs") + @APIResponse(responseCode = "401", description = "Non authentifié") + // @RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"}) // Temporairement désactivé + public Response listerEvenements( + @Parameter(description = "Numéro de page (0-based)", example = "0") + @QueryParam("page") + @DefaultValue("0") + @Min(0) + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + @Min(1) + int size, + @Parameter(description = "Champ de tri", example = "dateDebut") + @QueryParam("sort") + @DefaultValue("dateDebut") + String sortField, + @Parameter(description = "Direction du tri (asc/desc)", example = "asc") + @QueryParam("direction") + @DefaultValue("asc") + String sortDirection) { + + try { + LOG.infof("GET /api/evenements - page: %d, size: %d", page, size); + + Sort sort = + sortDirection.equalsIgnoreCase("desc") + ? Sort.by(sortField).descending() + : Sort.by(sortField).ascending(); + + List evenements = + evenementService.listerEvenementsActifs(Page.of(page, size), sort); + + LOG.infof("Nombre d'événements récupérés: %d", evenements.size()); + + // Convertir en DTO mobile + List evenementsDTOs = new ArrayList<>(); + for (Evenement evenement : evenements) { + try { + EvenementMobileDTO dto = EvenementMobileDTO.fromEntity(evenement); + evenementsDTOs.add(dto); + } catch (Exception e) { + LOG.errorf("Erreur lors de la conversion de l'événement %s: %s", evenement.getId(), e.getMessage()); + // Continuer avec les autres événements + } + } + + LOG.infof("Nombre de DTOs créés: %d", evenementsDTOs.size()); + + // Compter le total d'événements actifs + long total = evenementService.countEvenementsActifs(); + int totalPages = total > 0 ? (int) Math.ceil((double) total / size) : 0; + + // Retourner la structure paginée attendue par le mobile + Map response = new HashMap<>(); + response.put("data", evenementsDTOs); + response.put("total", total); + response.put("page", page); + response.put("size", size); + response.put("totalPages", totalPages); + + LOG.infof("Réponse prête: %d événements, total=%d, pages=%d", evenementsDTOs.size(), total, totalPages); + + return Response.ok(response) + .header("Content-Type", "application/json;charset=UTF-8") + .build(); + + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération des événements: %s", e.getMessage(), e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération des événements: " + e.getMessage())) + .build(); + } + } + + /** Récupère un événement par son ID */ + @GET + @Path("/{id}") + @Operation(summary = "Récupérer un événement par ID") + @APIResponse(responseCode = "200", description = "Événement trouvé") + @APIResponse(responseCode = "404", description = "Événement non trouvé") + @RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"}) + public Response obtenirEvenement( + @Parameter(description = "UUID de l'événement", required = true) @PathParam("id") UUID id) { + + try { + LOG.infof("GET /api/evenements/%s", id); + + Optional evenement = evenementService.trouverParId(id); + + if (evenement.isPresent()) { + return Response.ok(evenement.get()).build(); + } else { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Événement non trouvé")) + .build(); + } + + } catch (Exception e) { + LOG.errorf("Erreur lors de la récupération de l'événement %d: %s", id, e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération de l'événement")) + .build(); + } + } + + /** Crée un nouvel événement */ + @POST + @Operation(summary = "Créer un nouvel événement") + @APIResponse(responseCode = "201", description = "Événement créé avec succès") + @APIResponse(responseCode = "400", description = "Données invalides") + @RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT"}) + public Response creerEvenement( + @Parameter(description = "Données de l'événement à créer", required = true) @Valid + Evenement evenement) { + + try { + LOG.infof("POST /api/evenements - Création événement: %s", evenement.getTitre()); + + Evenement evenementCree = evenementService.creerEvenement(evenement); + + return Response.status(Response.Status.CREATED).entity(evenementCree).build(); + + } catch (IllegalArgumentException e) { + LOG.warnf("Données invalides: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (SecurityException e) { + LOG.warnf("Permissions insuffisantes: %s", e.getMessage()); + return Response.status(Response.Status.FORBIDDEN) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de la création: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la création de l'événement")) + .build(); + } + } + + /** Met à jour un événement existant */ + @PUT + @Path("/{id}") + @Operation(summary = "Mettre à jour un événement") + @APIResponse(responseCode = "200", description = "Événement mis à jour avec succès") + @APIResponse(responseCode = "404", description = "Événement non trouvé") + @RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT"}) + public Response mettreAJourEvenement(@PathParam("id") UUID id, @Valid Evenement evenement) { + + try { + LOG.infof("PUT /api/evenements/%s", id); + + Evenement evenementMisAJour = evenementService.mettreAJourEvenement(id, evenement); + + return Response.ok(evenementMisAJour).build(); + + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (SecurityException e) { + return Response.status(Response.Status.FORBIDDEN) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de la mise à jour: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la mise à jour")) + .build(); + } + } + + /** Supprime un événement */ + @DELETE + @Path("/{id}") + @Operation(summary = "Supprimer un événement") + @APIResponse(responseCode = "204", description = "Événement supprimé avec succès") + @RolesAllowed({"ADMIN", "PRESIDENT", "ORGANISATEUR_EVENEMENT"}) + public Response supprimerEvenement(@PathParam("id") UUID id) { + + try { + LOG.infof("DELETE /api/evenements/%s", id); + + evenementService.supprimerEvenement(id); + + return Response.noContent().build(); + + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (IllegalStateException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (SecurityException e) { + return Response.status(Response.Status.FORBIDDEN) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de la suppression: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la suppression")) + .build(); + } + } + + /** Endpoints spécialisés pour l'application mobile */ + + /** Liste les événements à venir */ + @GET + @Path("/a-venir") + @Operation(summary = "Événements à venir") + @RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"}) + public Response evenementsAVenir( + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("10") int size) { + + try { + List evenements = + evenementService.listerEvenementsAVenir( + Page.of(page, size), Sort.by("dateDebut").ascending()); + + return Response.ok(evenements).build(); + } catch (Exception e) { + LOG.errorf("Erreur événements à venir: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération")) + .build(); + } + } + + /** Liste les événements publics */ + @GET + @Path("/publics") + @Operation(summary = "Événements publics") + public Response evenementsPublics( + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size) { + + try { + List evenements = + evenementService.listerEvenementsPublics( + Page.of(page, size), Sort.by("dateDebut").ascending()); + + return Response.ok(evenements).build(); + } catch (Exception e) { + LOG.errorf("Erreur événements publics: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération")) + .build(); + } + } + + /** Recherche d'événements */ + @GET + @Path("/recherche") + @Operation(summary = "Rechercher des événements") + @RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"}) + public Response rechercherEvenements( + @QueryParam("q") String recherche, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size) { + + try { + if (recherche == null || recherche.trim().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Le terme de recherche est obligatoire")) + .build(); + } + + List evenements = + evenementService.rechercherEvenements( + recherche, Page.of(page, size), Sort.by("dateDebut").ascending()); + + return Response.ok(evenements).build(); + } catch (Exception e) { + LOG.errorf("Erreur recherche: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la recherche")) + .build(); + } + } + + /** Événements par type */ + @GET + @Path("/type/{type}") + @Operation(summary = "Événements par type") + @RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT", "MEMBRE"}) + public Response evenementsParType( + @PathParam("type") TypeEvenement type, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("size") @DefaultValue("20") int size) { + + try { + List evenements = + evenementService.listerParType( + type, Page.of(page, size), Sort.by("dateDebut").ascending()); + + return Response.ok(evenements).build(); + } catch (Exception e) { + LOG.errorf("Erreur événements par type: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la récupération")) + .build(); + } + } + + /** Change le statut d'un événement */ + @PATCH + @Path("/{id}/statut") + @Operation(summary = "Changer le statut d'un événement") + @RolesAllowed({"ADMIN", "PRESIDENT", "ORGANISATEUR_EVENEMENT"}) + public Response changerStatut( + @PathParam("id") UUID id, @QueryParam("statut") StatutEvenement nouveauStatut) { + + try { + if (nouveauStatut == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Le nouveau statut est obligatoire")) + .build(); + } + + Evenement evenement = evenementService.changerStatut(id, nouveauStatut); + + return Response.ok(evenement).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (SecurityException e) { + return Response.status(Response.Status.FORBIDDEN) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf("Erreur changement statut: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors du changement de statut")) + .build(); + } + } + + /** Statistiques des événements */ + @GET + @Path("/statistiques") + @Operation(summary = "Statistiques des événements") + @RolesAllowed({"ADMIN", "PRESIDENT", "SECRETAIRE", "ORGANISATEUR_EVENEMENT"}) + public Response obtenirStatistiques() { + + try { + Map statistiques = evenementService.obtenirStatistiques(); + + return Response.ok(statistiques).build(); + } catch (Exception e) { + LOG.errorf("Erreur statistiques: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors du calcul des statistiques")) + .build(); + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/ExportResource.java b/src/main/java/dev/lions/unionflow/server/resource/ExportResource.java new file mode 100644 index 0000000..452f278 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/ExportResource.java @@ -0,0 +1,119 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.service.ExportService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.annotation.security.RolesAllowed; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** Resource REST pour l'export des données */ +@Path("/api/export") +@ApplicationScoped +@Tag(name = "Export", description = "API d'export des données") +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class ExportResource { + + private static final Logger LOG = Logger.getLogger(ExportResource.class); + + @Inject ExportService exportService; + + @GET + @Path("/cotisations/csv") + @Produces("text/csv") + @Operation(summary = "Exporter les cotisations en CSV") + @APIResponse(responseCode = "200", description = "Fichier CSV généré") + public Response exporterCotisationsCSV( + @QueryParam("statut") String statut, + @QueryParam("type") String type, + @QueryParam("associationId") UUID associationId) { + LOG.info("Export CSV des cotisations"); + + byte[] csv = exportService.exporterToutesCotisationsCSV(statut, type, associationId); + + return Response.ok(csv) + .header("Content-Disposition", "attachment; filename=\"cotisations.csv\"") + .header("Content-Type", "text/csv; charset=UTF-8") + .build(); + } + + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/cotisations/csv") + @Consumes(MediaType.APPLICATION_JSON) + @Produces("text/csv") + @Operation(summary = "Exporter des cotisations spécifiques en CSV") + @APIResponse(responseCode = "200", description = "Fichier CSV généré") + public Response exporterCotisationsSelectionneesCSV(List cotisationIds) { + LOG.infof("Export CSV de %d cotisations", cotisationIds.size()); + + byte[] csv = exportService.exporterCotisationsCSV(cotisationIds); + + return Response.ok(csv) + .header("Content-Disposition", "attachment; filename=\"cotisations.csv\"") + .header("Content-Type", "text/csv; charset=UTF-8") + .build(); + } + + @GET + @Path("/cotisations/{cotisationId}/recu") + @Produces("text/plain") + @Operation(summary = "Générer un reçu de paiement") + @APIResponse(responseCode = "200", description = "Reçu généré") + public Response genererRecu(@PathParam("cotisationId") UUID cotisationId) { + LOG.infof("Génération reçu pour: %s", cotisationId); + + byte[] recu = exportService.genererRecuPaiement(cotisationId); + + return Response.ok(recu) + .header("Content-Disposition", "attachment; filename=\"recu-" + cotisationId + ".txt\"") + .header("Content-Type", "text/plain; charset=UTF-8") + .build(); + } + + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/cotisations/recus") + @Consumes(MediaType.APPLICATION_JSON) + @Produces("text/plain") + @Operation(summary = "Générer des reçus groupés") + @APIResponse(responseCode = "200", description = "Reçus générés") + public Response genererRecusGroupes(List cotisationIds) { + LOG.infof("Génération de %d reçus", cotisationIds.size()); + + byte[] recus = exportService.genererRecusGroupes(cotisationIds); + + return Response.ok(recus) + .header("Content-Disposition", "attachment; filename=\"recus-groupes.txt\"") + .header("Content-Type", "text/plain; charset=UTF-8") + .build(); + } + + @GET + @Path("/rapport/mensuel") + @Produces("text/plain") + @Operation(summary = "Générer un rapport mensuel") + @APIResponse(responseCode = "200", description = "Rapport généré") + public Response genererRapportMensuel( + @QueryParam("annee") int annee, + @QueryParam("mois") int mois, + @QueryParam("associationId") UUID associationId) { + LOG.infof("Génération rapport mensuel: %d/%d", mois, annee); + + byte[] rapport = exportService.genererRapportMensuel(annee, mois, associationId); + + return Response.ok(rapport) + .header("Content-Disposition", + "attachment; filename=\"rapport-" + annee + "-" + String.format("%02d", mois) + ".txt\"") + .header("Content-Type", "text/plain; charset=UTF-8") + .build(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/resource/HealthResource.java b/src/main/java/dev/lions/unionflow/server/resource/HealthResource.java new file mode 100644 index 0000000..85536a4 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/HealthResource.java @@ -0,0 +1,33 @@ +package dev.lions.unionflow.server.resource; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.time.LocalDateTime; +import java.util.Map; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +/** Resource de santé pour UnionFlow Server */ +@Path("/api/status") +@Produces(MediaType.APPLICATION_JSON) +@ApplicationScoped +@Tag(name = "Status", description = "API de statut du serveur") +public class HealthResource { + + @GET + @Operation(summary = "Vérifier le statut du serveur") + public Response getStatus() { + return Response.ok( + Map.of( + "status", "UP", + "service", "UnionFlow Server", + "version", "1.0.0", + "timestamp", LocalDateTime.now().toString(), + "message", "Serveur opérationnel")) + .build(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java b/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java new file mode 100644 index 0000000..785ae61 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/MembreResource.java @@ -0,0 +1,643 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.membre.MembreDTO; +import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria; +import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.service.MembreService; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** Resource REST pour la gestion des membres */ +@Path("/api/membres") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@ApplicationScoped +@Tag(name = "Membres", description = "API de gestion des membres") +public class MembreResource { + + private static final Logger LOG = Logger.getLogger(MembreResource.class); + + @Inject MembreService membreService; + + @GET + @Operation(summary = "Lister tous les membres actifs") + @APIResponse(responseCode = "200", description = "Liste des membres actifs") + public Response listerMembres( + @Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") + int page, + @Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") + int size, + @Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom") + String sortField, + @Parameter(description = "Direction du tri (asc/desc)") + @QueryParam("direction") + @DefaultValue("asc") + String sortDirection) { + + LOG.infof("Récupération de la liste des membres actifs - page: %d, size: %d", page, size); + + Sort sort = + "desc".equalsIgnoreCase(sortDirection) + ? Sort.by(sortField).descending() + : Sort.by(sortField).ascending(); + + List membres = membreService.listerMembresActifs(Page.of(page, size), sort); + List membresDTO = membreService.convertToDTOList(membres); + + return Response.ok(membresDTO).build(); + } + + @GET + @Path("/{id}") + @Operation(summary = "Récupérer un membre par son ID") + @APIResponse(responseCode = "200", description = "Membre trouvé") + @APIResponse(responseCode = "404", description = "Membre non trouvé") + public Response obtenirMembre(@Parameter(description = "UUID du membre") @PathParam("id") UUID id) { + LOG.infof("Récupération du membre ID: %s", id); + return membreService + .trouverParId(id) + .map( + membre -> { + MembreDTO membreDTO = membreService.convertToDTO(membre); + return Response.ok(membreDTO).build(); + }) + .orElse( + Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("message", "Membre non trouvé")) + .build()); + } + + @POST + @PermitAll + @Operation(summary = "Créer un nouveau membre") + @APIResponse(responseCode = "201", description = "Membre créé avec succès") + @APIResponse(responseCode = "400", description = "Données invalides") + public Response creerMembre(@Valid MembreDTO membreDTO) { + LOG.infof("Création d'un nouveau membre: %s", membreDTO.getEmail()); + try { + // Conversion DTO vers entité + Membre membre = membreService.convertFromDTO(membreDTO); + + // Création du membre + Membre nouveauMembre = membreService.creerMembre(membre); + + // Conversion de retour vers DTO + MembreDTO nouveauMembreDTO = membreService.convertToDTO(nouveauMembre); + + return Response.status(Response.Status.CREATED).entity(nouveauMembreDTO).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", e.getMessage())) + .build(); + } + } + + @PUT + @Path("/{id}") + @Operation(summary = "Mettre à jour un membre existant") + @APIResponse(responseCode = "200", description = "Membre mis à jour avec succès") + @APIResponse(responseCode = "404", description = "Membre non trouvé") + @APIResponse(responseCode = "400", description = "Données invalides") + public Response mettreAJourMembre( + @Parameter(description = "UUID du membre") @PathParam("id") UUID id, + @Valid MembreDTO membreDTO) { + LOG.infof("Mise à jour du membre ID: %s", id); + try { + // Conversion DTO vers entité + Membre membre = membreService.convertFromDTO(membreDTO); + + // Mise à jour du membre + Membre membreMisAJour = membreService.mettreAJourMembre(id, membre); + + // Conversion de retour vers DTO + MembreDTO membreMisAJourDTO = membreService.convertToDTO(membreMisAJour); + + return Response.ok(membreMisAJourDTO).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", e.getMessage())) + .build(); + } + } + + @DELETE + @Path("/{id}") + @Operation(summary = "Désactiver un membre") + @APIResponse(responseCode = "204", description = "Membre désactivé avec succès") + @APIResponse(responseCode = "404", description = "Membre non trouvé") + public Response desactiverMembre( + @Parameter(description = "UUID du membre") @PathParam("id") UUID id) { + LOG.infof("Désactivation du membre ID: %s", id); + try { + membreService.desactiverMembre(id); + return Response.noContent().build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("message", e.getMessage())) + .build(); + } + } + + @GET + @Path("/recherche") + @Operation(summary = "Rechercher des membres par nom ou prénom") + @APIResponse(responseCode = "200", description = "Résultats de la recherche") + public Response rechercherMembres( + @Parameter(description = "Terme de recherche") @QueryParam("q") String recherche, + @Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") + int page, + @Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") + int size, + @Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom") + String sortField, + @Parameter(description = "Direction du tri (asc/desc)") + @QueryParam("direction") + @DefaultValue("asc") + String sortDirection) { + + LOG.infof("Recherche de membres avec le terme: %s", recherche); + if (recherche == null || recherche.trim().isEmpty()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", "Le terme de recherche est requis")) + .build(); + } + + Sort sort = + "desc".equalsIgnoreCase(sortDirection) + ? Sort.by(sortField).descending() + : Sort.by(sortField).ascending(); + + List membres = + membreService.rechercherMembres(recherche.trim(), Page.of(page, size), sort); + List membresDTO = membreService.convertToDTOList(membres); + + return Response.ok(membresDTO).build(); + } + + @GET + @Path("/stats") + @Operation(summary = "Obtenir les statistiques avancées des membres") + @APIResponse(responseCode = "200", description = "Statistiques complètes des membres") + public Response obtenirStatistiques() { + LOG.info("Récupération des statistiques avancées des membres"); + Map statistiques = membreService.obtenirStatistiquesAvancees(); + return Response.ok(statistiques).build(); + } + + @GET + @Path("/autocomplete/villes") + @Operation(summary = "Obtenir la liste des villes pour autocomplétion") + @APIResponse(responseCode = "200", description = "Liste des villes distinctes") + public Response obtenirVilles( + @Parameter(description = "Terme de recherche (optionnel)") @QueryParam("query") String query) { + LOG.infof("Récupération des villes pour autocomplétion - query: %s", query); + List villes = membreService.obtenirVillesDistinctes(query); + return Response.ok(villes).build(); + } + + @GET + @Path("/autocomplete/professions") + @Operation(summary = "Obtenir la liste des professions pour autocomplétion") + @APIResponse(responseCode = "200", description = "Liste des professions distinctes") + public Response obtenirProfessions( + @Parameter(description = "Terme de recherche (optionnel)") @QueryParam("query") String query) { + LOG.infof("Récupération des professions pour autocomplétion - query: %s", query); + List professions = membreService.obtenirProfessionsDistinctes(query); + return Response.ok(professions).build(); + } + + @GET + @Path("/recherche-avancee") + @Operation(summary = "Recherche avancée de membres avec filtres multiples (DEPRECATED)") + @APIResponse(responseCode = "200", description = "Résultats de la recherche avancée") + @Deprecated + public Response rechercheAvancee( + @Parameter(description = "Terme de recherche") @QueryParam("q") String recherche, + @Parameter(description = "Statut actif (true/false)") @QueryParam("actif") Boolean actif, + @Parameter(description = "Date d'adhésion minimum (YYYY-MM-DD)") + @QueryParam("dateAdhesionMin") + String dateAdhesionMin, + @Parameter(description = "Date d'adhésion maximum (YYYY-MM-DD)") + @QueryParam("dateAdhesionMax") + String dateAdhesionMax, + @Parameter(description = "Numéro de page (0-based)") @QueryParam("page") @DefaultValue("0") + int page, + @Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") + int size, + @Parameter(description = "Champ de tri") @QueryParam("sort") @DefaultValue("nom") + String sortField, + @Parameter(description = "Direction du tri (asc/desc)") + @QueryParam("direction") + @DefaultValue("asc") + String sortDirection) { + + LOG.infof( + "Recherche avancée de membres (DEPRECATED) - recherche: %s, actif: %s", recherche, actif); + + try { + Sort sort = + "desc".equalsIgnoreCase(sortDirection) + ? Sort.by(sortField).descending() + : Sort.by(sortField).ascending(); + + // Conversion des dates si fournies + java.time.LocalDate dateMin = + dateAdhesionMin != null ? java.time.LocalDate.parse(dateAdhesionMin) : null; + java.time.LocalDate dateMax = + dateAdhesionMax != null ? java.time.LocalDate.parse(dateAdhesionMax) : null; + + List membres = + membreService.rechercheAvancee( + recherche, actif, dateMin, dateMax, Page.of(page, size), sort); + List membresDTO = membreService.convertToDTOList(membres); + + return Response.ok(membresDTO).build(); + } catch (Exception e) { + LOG.errorf("Erreur lors de la recherche avancée: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", "Erreur dans les paramètres de recherche: " + e.getMessage())) + .build(); + } + } + + /** + * Nouvelle recherche avancée avec critères complets et résultats enrichis Réservée aux super + * administrateurs pour des recherches sophistiquées + */ + @POST + @Path("/search/advanced") + @RolesAllowed({"SUPER_ADMIN", "ADMIN"}) + @Operation( + summary = "Recherche avancée de membres avec critères multiples", + description = + """ + Recherche sophistiquée de membres avec de nombreux critères de filtrage : + - Recherche textuelle dans nom, prénom, email + - Filtres par organisation, rôles, statut + - Filtres par âge, région, profession + - Filtres par dates d'adhésion + - Résultats paginés avec statistiques + + Réservée aux super administrateurs et administrateurs. + """) + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Recherche effectuée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = MembreSearchResultDTO.class), + examples = + @ExampleObject( + name = "Exemple de résultats", + value = + """ + { + "membres": [...], + "totalElements": 247, + "totalPages": 13, + "currentPage": 0, + "pageSize": 20, + "hasNext": true, + "hasPrevious": false, + "executionTimeMs": 45, + "statistics": { + "membresActifs": 230, + "membresInactifs": 17, + "ageMoyen": 34.5, + "nombreOrganisations": 12 + } + } + """))), + @APIResponse( + responseCode = "400", + description = "Critères de recherche invalides", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + examples = + @ExampleObject( + value = + """ +{ + "message": "Critères de recherche invalides", + "details": "La date minimum ne peut pas être postérieure à la date maximum" +} +"""))), + @APIResponse( + responseCode = "403", + description = "Accès non autorisé - Rôle SUPER_ADMIN ou ADMIN requis"), + @APIResponse(responseCode = "500", description = "Erreur interne du serveur") + }) + @SecurityRequirement(name = "keycloak") + public Response searchMembresAdvanced( + @RequestBody( + description = "Critères de recherche avancée", + required = false, + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = MembreSearchCriteria.class), + examples = + @ExampleObject( + name = "Exemple de critères", + value = + """ + { + "query": "marie", + "statut": "ACTIF", + "ageMin": 25, + "ageMax": 45, + "region": "Dakar", + "roles": ["PRESIDENT", "SECRETAIRE"], + "dateAdhesionMin": "2020-01-01", + "includeInactifs": false + } + """))) + MembreSearchCriteria criteria, + @Parameter(description = "Numéro de page (0-based)", example = "0") + @QueryParam("page") + @DefaultValue("0") + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + int size, + @Parameter(description = "Champ de tri", example = "nom") + @QueryParam("sort") + @DefaultValue("nom") + String sortField, + @Parameter(description = "Direction du tri (asc/desc)", example = "asc") + @QueryParam("direction") + @DefaultValue("asc") + String sortDirection) { + + long startTime = System.currentTimeMillis(); + + try { + // Validation des critères + if (criteria == null) { + LOG.warn("Recherche avancée de membres - critères null rejetés"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", "Les critères de recherche sont requis")) + .build(); + } + + LOG.infof( + "Recherche avancée de membres - critères: %s, page: %d, size: %d", + criteria.getDescription(), page, size); + + // Nettoyage et validation des critères + criteria.sanitize(); + + if (!criteria.hasAnyCriteria()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", "Au moins un critère de recherche doit être spécifié")) + .build(); + } + + if (!criteria.isValid()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity( + Map.of( + "message", "Critères de recherche invalides", + "details", "Vérifiez la cohérence des dates et des âges")) + .build(); + } + + // Construction du tri + Sort sort = + "desc".equalsIgnoreCase(sortDirection) + ? Sort.by(sortField).descending() + : Sort.by(sortField).ascending(); + + // Exécution de la recherche + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(page, size), sort); + + // Calcul du temps d'exécution + long executionTime = System.currentTimeMillis() - startTime; + result.setExecutionTimeMs(executionTime); + + LOG.infof( + "Recherche avancée terminée - %d résultats trouvés en %d ms", + result.getTotalElements(), executionTime); + + return Response.ok(result).build(); + + } catch (jakarta.validation.ConstraintViolationException e) { + LOG.warnf("Erreur de validation Jakarta dans la recherche avancée: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", "Critères de recherche invalides", "details", e.getMessage())) + .build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Erreur de validation dans la recherche avancée: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("message", "Paramètres de recherche invalides", "details", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche avancée de membres"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("message", "Erreur interne lors de la recherche", "error", e.getMessage())) + .build(); + } + } + + @POST + @Path("/export/selection") + @Consumes(MediaType.APPLICATION_JSON) + @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + @Operation(summary = "Exporter une sélection de membres en Excel") + @APIResponse(responseCode = "200", description = "Fichier Excel généré") + public Response exporterSelectionMembres( + @Parameter(description = "Liste des IDs des membres à exporter") List membreIds, + @Parameter(description = "Format d'export") @QueryParam("format") @DefaultValue("EXCEL") String format) { + LOG.infof("Export de %d membres sélectionnés", membreIds.size()); + try { + byte[] excelData = membreService.exporterMembresSelectionnes(membreIds, format); + return Response.ok(excelData) + .header("Content-Disposition", "attachment; filename=\"membres_selection_" + + java.time.LocalDate.now() + "." + (format != null ? format.toLowerCase() : "xlsx") + "\"") + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'export de la sélection"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de l'export: " + e.getMessage())) + .build(); + } + } + + @POST + @Path("/import") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Importer des membres depuis un fichier Excel ou CSV") + @APIResponse(responseCode = "200", description = "Import terminé") + public Response importerMembres( + @Parameter(description = "Contenu du fichier à importer") @FormParam("file") byte[] fileContent, + @Parameter(description = "Nom du fichier") @FormParam("fileName") String fileName, + @Parameter(description = "ID de l'organisation (optionnel)") @FormParam("organisationId") UUID organisationId, + @Parameter(description = "Type de membre par défaut") @FormParam("typeMembreDefaut") String typeMembreDefaut, + @Parameter(description = "Mettre à jour les membres existants") @FormParam("mettreAJourExistants") boolean mettreAJourExistants, + @Parameter(description = "Ignorer les erreurs") @FormParam("ignorerErreurs") boolean ignorerErreurs) { + + try { + if (fileContent == null || fileContent.length == 0) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", "Aucun fichier fourni")) + .build(); + } + + if (fileName == null || fileName.isEmpty()) { + fileName = "import.xlsx"; + } + + if (typeMembreDefaut == null || typeMembreDefaut.isEmpty()) { + typeMembreDefaut = "ACTIF"; + } + + InputStream fileInputStream = new java.io.ByteArrayInputStream(fileContent); + dev.lions.unionflow.server.service.MembreImportExportService.ResultatImport resultat = membreService.importerMembres( + fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs); + + Map response = new HashMap<>(); + response.put("totalLignes", resultat.totalLignes); + response.put("lignesTraitees", resultat.lignesTraitees); + response.put("lignesErreur", resultat.lignesErreur); + response.put("erreurs", resultat.erreurs); + response.put("membresImportes", resultat.membresImportes); + + return Response.ok(response).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'import"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de l'import: " + e.getMessage())) + .build(); + } + } + + @GET + @Path("/export") + @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + @Operation(summary = "Exporter des membres en Excel, CSV ou PDF") + @APIResponse(responseCode = "200", description = "Fichier exporté") + public Response exporterMembres( + @Parameter(description = "Format d'export") @QueryParam("format") @DefaultValue("EXCEL") String format, + @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("associationId") UUID associationId, + @Parameter(description = "Statut des membres") @QueryParam("statut") String statut, + @Parameter(description = "Type de membre") @QueryParam("type") String type, + @Parameter(description = "Date adhésion début") @QueryParam("dateAdhesionDebut") String dateAdhesionDebut, + @Parameter(description = "Date adhésion fin") @QueryParam("dateAdhesionFin") String dateAdhesionFin, + @Parameter(description = "Colonnes à exporter") @QueryParam("colonnes") List colonnesExportList, + @Parameter(description = "Inclure les en-têtes") @QueryParam("inclureHeaders") @DefaultValue("true") boolean inclureHeaders, + @Parameter(description = "Formater les dates") @QueryParam("formaterDates") @DefaultValue("true") boolean formaterDates, + @Parameter(description = "Inclure un onglet statistiques (Excel uniquement)") @QueryParam("inclureStatistiques") @DefaultValue("false") boolean inclureStatistiques, + @Parameter(description = "Mot de passe pour chiffrer le fichier (optionnel)") @QueryParam("motDePasse") String motDePasse) { + + try { + // Récupérer les membres selon les filtres + List membres = membreService.listerMembresPourExport( + associationId, statut, type, dateAdhesionDebut, dateAdhesionFin); + + byte[] exportData; + String contentType; + String extension; + + List colonnesExport = colonnesExportList != null ? colonnesExportList : new ArrayList<>(); + + if ("CSV".equalsIgnoreCase(format)) { + exportData = membreService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates); + contentType = "text/csv"; + extension = "csv"; + } else { + // Pour Excel, inclure les statistiques uniquement si demandé et si format Excel + boolean stats = inclureStatistiques && "EXCEL".equalsIgnoreCase(format); + exportData = membreService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates, stats, motDePasse); + contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + extension = "xlsx"; + } + + return Response.ok(exportData) + .type(contentType) + .header("Content-Disposition", "attachment; filename=\"membres_export_" + + java.time.LocalDate.now() + "." + extension + "\"") + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'export"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de l'export: " + e.getMessage())) + .build(); + } + } + + @GET + @Path("/import/modele") + @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + @Operation(summary = "Télécharger le modèle Excel pour l'import") + @APIResponse(responseCode = "200", description = "Modèle Excel généré") + public Response telechargerModeleImport() { + try { + byte[] modele = membreService.genererModeleImport(); + return Response.ok(modele) + .header("Content-Disposition", "attachment; filename=\"modele_import_membres.xlsx\"") + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la génération du modèle"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors de la génération du modèle: " + e.getMessage())) + .build(); + } + } + + @GET + @Path("/export/count") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Compter les membres selon les filtres pour l'export") + @APIResponse(responseCode = "200", description = "Nombre de membres correspondant aux critères") + public Response compterMembresPourExport( + @Parameter(description = "ID de l'organisation (optionnel)") @QueryParam("associationId") UUID associationId, + @Parameter(description = "Statut des membres") @QueryParam("statut") String statut, + @Parameter(description = "Type de membre") @QueryParam("type") String type, + @Parameter(description = "Date adhésion début") @QueryParam("dateAdhesionDebut") String dateAdhesionDebut, + @Parameter(description = "Date adhésion fin") @QueryParam("dateAdhesionFin") String dateAdhesionFin) { + + try { + List membres = membreService.listerMembresPourExport( + associationId, statut, type, dateAdhesionDebut, dateAdhesionFin); + + return Response.ok(Map.of("count", membres.size())).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du comptage des membres"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur lors du comptage: " + e.getMessage())) + .build(); + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/NotificationResource.java b/src/main/java/dev/lions/unionflow/server/resource/NotificationResource.java new file mode 100644 index 0000000..a0158bc --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/NotificationResource.java @@ -0,0 +1,246 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.notification.NotificationDTO; +import dev.lions.unionflow.server.api.dto.notification.TemplateNotificationDTO; +import dev.lions.unionflow.server.service.NotificationService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Resource REST pour la gestion des notifications + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Path("/api/notifications") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class NotificationResource { + + private static final Logger LOG = Logger.getLogger(NotificationResource.class); + + @Inject NotificationService notificationService; + + // ======================================== + // TEMPLATES + // ======================================== + + /** + * Crée un nouveau template de notification + * + * @param templateDTO DTO du template à créer + * @return Template créé + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/templates") + public Response creerTemplate(@Valid TemplateNotificationDTO templateDTO) { + try { + TemplateNotificationDTO result = notificationService.creerTemplate(templateDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création du template"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création du template: " + e.getMessage())) + .build(); + } + } + + // ======================================== + // NOTIFICATIONS + // ======================================== + + /** + * Crée une nouvelle notification + * + * @param notificationDTO DTO de la notification à créer + * @return Notification créée + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + public Response creerNotification(@Valid NotificationDTO notificationDTO) { + try { + NotificationDTO result = notificationService.creerNotification(notificationDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création de la notification"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création de la notification: " + e.getMessage())) + .build(); + } + } + + /** + * Marque une notification comme lue + * + * @param id ID de la notification + * @return Notification mise à jour + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}/marquer-lue") + public Response marquerCommeLue(@PathParam("id") UUID id) { + try { + NotificationDTO result = notificationService.marquerCommeLue(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Notification non trouvée")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du marquage de la notification"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors du marquage de la notification: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve une notification par son ID + * + * @param id ID de la notification + * @return Notification + */ + @GET + @Path("/{id}") + public Response trouverNotificationParId(@PathParam("id") UUID id) { + try { + NotificationDTO result = notificationService.trouverNotificationParId(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Notification non trouvée")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche de la notification"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche de la notification: " + e.getMessage())) + .build(); + } + } + + /** + * Liste toutes les notifications d'un membre + * + * @param membreId ID du membre + * @return Liste des notifications + */ + @GET + @Path("/membre/{membreId}") + public Response listerNotificationsParMembre(@PathParam("membreId") UUID membreId) { + try { + List result = notificationService.listerNotificationsParMembre(membreId); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des notifications"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des notifications: " + e.getMessage())) + .build(); + } + } + + /** + * Liste les notifications non lues d'un membre + * + * @param membreId ID du membre + * @return Liste des notifications non lues + */ + @GET + @Path("/membre/{membreId}/non-lues") + public Response listerNotificationsNonLuesParMembre(@PathParam("membreId") UUID membreId) { + try { + List result = notificationService.listerNotificationsNonLuesParMembre(membreId); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des notifications non lues"); + return Response.status(Response.Status.BAD_REQUEST) + .entity( + new ErrorResponse( + "Erreur lors de la liste des notifications non lues: " + e.getMessage())) + .build(); + } + } + + /** + * Liste les notifications en attente d'envoi + * + * @return Liste des notifications en attente + */ + @GET + @Path("/en-attente-envoi") + public Response listerNotificationsEnAttenteEnvoi() { + try { + List result = notificationService.listerNotificationsEnAttenteEnvoi(); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des notifications en attente"); + return Response.status(Response.Status.BAD_REQUEST) + .entity( + new ErrorResponse( + "Erreur lors de la liste des notifications en attente: " + e.getMessage())) + .build(); + } + } + + /** + * Envoie des notifications groupées à plusieurs membres (WOU/DRY) + * + * @param request DTO contenant les IDs des membres, sujet, corps et canaux + * @return Nombre de notifications créées + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/groupees") + public Response envoyerNotificationsGroupees(NotificationGroupeeRequest request) { + try { + int notificationsCreees = + notificationService.envoyerNotificationsGroupees( + request.membreIds, request.sujet, request.corps, request.canaux); + return Response.ok(Map.of("notificationsCreees", notificationsCreees)).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'envoi des notifications groupées"); + return Response.status(Response.Status.BAD_REQUEST) + .entity( + new ErrorResponse( + "Erreur lors de l'envoi des notifications groupées: " + e.getMessage())) + .build(); + } + } + + /** Classe interne pour les réponses d'erreur */ + public static class ErrorResponse { + public String error; + + public ErrorResponse(String error) { + this.error = error; + } + } + + /** Classe interne pour les requêtes de notifications groupées (WOU/DRY) */ + public static class NotificationGroupeeRequest { + public List membreIds; + public String sujet; + public String corps; + public List canaux; + + public NotificationGroupeeRequest() {} + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java b/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java new file mode 100644 index 0000000..b3126ab --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/OrganisationResource.java @@ -0,0 +1,423 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.service.KeycloakService; +import dev.lions.unionflow.server.service.OrganisationService; +import io.quarkus.security.Authenticated; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** + * Resource REST pour la gestion des organisations + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@Path("/api/organisations") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Organisations", description = "Gestion des organisations") +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class OrganisationResource { + + private static final Logger LOG = Logger.getLogger(OrganisationResource.class); + + @Inject OrganisationService organisationService; + + @Inject KeycloakService keycloakService; + + /** Crée une nouvelle organisation */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + + @Operation( + summary = "Créer une nouvelle organisation", + description = "Crée une nouvelle organisation dans le système") + @APIResponses({ + @APIResponse( + responseCode = "201", + description = "Organisation créée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = OrganisationDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "409", description = "Organisation déjà existante"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response creerOrganisation(@Valid OrganisationDTO organisationDTO) { + LOG.infof("Création d'une nouvelle organisation: %s", organisationDTO.getNom()); + + try { + Organisation organisation = organisationService.convertFromDTO(organisationDTO); + Organisation organisationCreee = organisationService.creerOrganisation(organisation); + OrganisationDTO dto = organisationService.convertToDTO(organisationCreee); + + return Response.created(URI.create("/api/organisations/" + organisationCreee.getId())) + .entity(dto) + .build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Erreur lors de la création de l'organisation: %s", e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de la création de l'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Récupère toutes les organisations actives */ + @GET + @jakarta.annotation.security.PermitAll // ✅ Accès public pour inscription + @Operation( + summary = "Lister les organisations", + description = "Récupère la liste des organisations actives avec pagination") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Liste des organisations récupérée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = OrganisationDTO.class))), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response listerOrganisations( + @Parameter(description = "Numéro de page (commence à 0)", example = "0") + @QueryParam("page") + @DefaultValue("0") + int page, + @Parameter(description = "Taille de la page", example = "20") + @QueryParam("size") + @DefaultValue("20") + int size, + @Parameter(description = "Terme de recherche (nom ou nom court)") @QueryParam("recherche") + String recherche) { + + LOG.infof( + "Récupération des organisations - page: %d, size: %d, recherche: %s", + page, size, recherche); + + try { + List organisations; + + if (recherche != null && !recherche.trim().isEmpty()) { + organisations = organisationService.rechercherOrganisations(recherche.trim(), page, size); + } else { + organisations = organisationService.listerOrganisationsActives(page, size); + } + + List dtos = + organisations.stream() + .map(organisationService::convertToDTO) + .collect(Collectors.toList()); + + return Response.ok(dtos).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la récupération des organisations"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Récupère une organisation par son ID */ + @GET + @Path("/{id}") + + @Operation( + summary = "Récupérer une organisation", + description = "Récupère une organisation par son ID") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Organisation trouvée", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = OrganisationDTO.class))), + @APIResponse(responseCode = "404", description = "Organisation non trouvée"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response obtenirOrganisation( + @Parameter(description = "UUID de l'organisation", required = true) @PathParam("id") UUID id) { + + LOG.infof("Récupération de l'organisation ID: %d", id); + + return organisationService + .trouverParId(id) + .map( + organisation -> { + OrganisationDTO dto = organisationService.convertToDTO(organisation); + return Response.ok(dto).build(); + }) + .orElse( + Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", "Organisation non trouvée")) + .build()); + } + + /** Met à jour une organisation */ + @PUT + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}") + + @Operation( + summary = "Mettre à jour une organisation", + description = "Met à jour les informations d'une organisation") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Organisation mise à jour avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = OrganisationDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Organisation non trouvée"), + @APIResponse(responseCode = "409", description = "Conflit de données"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response mettreAJourOrganisation( + @Parameter(description = "UUID de l'organisation", required = true) @PathParam("id") UUID id, + @Valid OrganisationDTO organisationDTO) { + + LOG.infof("Mise à jour de l'organisation ID: %s", id); + + try { + Organisation organisationMiseAJour = organisationService.convertFromDTO(organisationDTO); + Organisation organisation = + organisationService.mettreAJourOrganisation(id, organisationMiseAJour, "system"); + OrganisationDTO dto = organisationService.convertToDTO(organisation); + + return Response.ok(dto).build(); + } catch (NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Erreur lors de la mise à jour de l'organisation: %s", e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de la mise à jour de l'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Supprime une organisation */ + @DELETE + @RolesAllowed({"ADMIN"}) + @Path("/{id}") + + @Operation( + summary = "Supprimer une organisation", + description = "Supprime une organisation (soft delete)") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Organisation supprimée avec succès"), + @APIResponse(responseCode = "404", description = "Organisation non trouvée"), + @APIResponse(responseCode = "409", description = "Impossible de supprimer l'organisation"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response supprimerOrganisation( + @Parameter(description = "UUID de l'organisation", required = true) @PathParam("id") UUID id) { + + LOG.infof("Suppression de l'organisation ID: %d", id); + + try { + organisationService.supprimerOrganisation(id, "system"); + return Response.noContent().build(); + } catch (NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (IllegalStateException e) { + LOG.warnf("Erreur lors de la suppression de l'organisation: %s", e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de la suppression de l'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Recherche avancée d'organisations */ + @GET + @Path("/recherche") + + @Operation( + summary = "Recherche avancée", + description = "Recherche d'organisations avec critères multiples") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Résultats de recherche", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(type = SchemaType.ARRAY, implementation = OrganisationDTO.class))), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response rechercheAvancee( + @Parameter(description = "Nom de l'organisation") @QueryParam("nom") String nom, + @Parameter(description = "Type d'organisation") @QueryParam("type") String typeOrganisation, + @Parameter(description = "Statut") @QueryParam("statut") String statut, + @Parameter(description = "Ville") @QueryParam("ville") String ville, + @Parameter(description = "Région") @QueryParam("region") String region, + @Parameter(description = "Pays") @QueryParam("pays") String pays, + @Parameter(description = "Numéro de page") @QueryParam("page") @DefaultValue("0") int page, + @Parameter(description = "Taille de la page") @QueryParam("size") @DefaultValue("20") + int size) { + + LOG.infof("Recherche avancée d'organisations avec critères multiples"); + + try { + List organisations = + organisationService.rechercheAvancee( + nom, typeOrganisation, statut, ville, region, pays, page, size); + + List dtos = + organisations.stream() + .map(organisationService::convertToDTO) + .collect(Collectors.toList()); + + return Response.ok(dtos).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche avancée"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Active une organisation */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}/activer") + + @Operation( + summary = "Activer une organisation", + description = "Active une organisation suspendue") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Organisation activée avec succès"), + @APIResponse(responseCode = "404", description = "Organisation non trouvée"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response activerOrganisation( + @Parameter(description = "UUID de l'organisation", required = true) @PathParam("id") UUID id) { + + LOG.infof("Activation de l'organisation ID: %d", id); + + try { + Organisation organisation = organisationService.activerOrganisation(id, "system"); + OrganisationDTO dto = organisationService.convertToDTO(organisation); + return Response.ok(dto).build(); + } catch (NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'activation de l'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Suspend une organisation */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}/suspendre") + + @Operation( + summary = "Suspendre une organisation", + description = "Suspend une organisation active") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Organisation suspendue avec succès"), + @APIResponse(responseCode = "404", description = "Organisation non trouvée"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response suspendreOrganisation( + @Parameter(description = "UUID de l'organisation", required = true) @PathParam("id") UUID id) { + + LOG.infof("Suspension de l'organisation ID: %d", id); + + try { + Organisation organisation = organisationService.suspendreOrganisation(id, "system"); + OrganisationDTO dto = organisationService.convertToDTO(organisation); + return Response.ok(dto).build(); + } catch (NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la suspension de l'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Obtient les statistiques des organisations */ + @GET + @Path("/statistiques") + + @Operation( + summary = "Statistiques des organisations", + description = "Récupère les statistiques globales des organisations") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Statistiques récupérées avec succès"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response obtenirStatistiques() { + LOG.info("Récupération des statistiques des organisations"); + + try { + Map statistiques = organisationService.obtenirStatistiques(); + return Response.ok(statistiques).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la récupération des statistiques"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java b/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java new file mode 100644 index 0000000..c732483 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/PaiementResource.java @@ -0,0 +1,213 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.paiement.PaiementDTO; +import dev.lions.unionflow.server.service.PaiementService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Resource REST pour la gestion des paiements + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Path("/api/paiements") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class PaiementResource { + + private static final Logger LOG = Logger.getLogger(PaiementResource.class); + + @Inject PaiementService paiementService; + + /** + * Crée un nouveau paiement + * + * @param paiementDTO DTO du paiement à créer + * @return Paiement créé + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + public Response creerPaiement(@Valid PaiementDTO paiementDTO) { + try { + PaiementDTO result = paiementService.creerPaiement(paiementDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Met à jour un paiement + * + * @param id ID du paiement + * @param paiementDTO DTO avec les modifications + * @return Paiement mis à jour + */ + @PUT + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}") + public Response mettreAJourPaiement(@PathParam("id") UUID id, @Valid PaiementDTO paiementDTO) { + try { + PaiementDTO result = paiementService.mettreAJourPaiement(id, paiementDTO); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (IllegalStateException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la mise à jour du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la mise à jour du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Valide un paiement + * + * @param id ID du paiement + * @return Paiement validé + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}/valider") + public Response validerPaiement(@PathParam("id") UUID id) { + try { + PaiementDTO result = paiementService.validerPaiement(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la validation du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la validation du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Annule un paiement + * + * @param id ID du paiement + * @return Paiement annulé + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}/annuler") + public Response annulerPaiement(@PathParam("id") UUID id) { + try { + PaiementDTO result = paiementService.annulerPaiement(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (IllegalStateException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'annulation du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de l'annulation du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un paiement par son ID + * + * @param id ID du paiement + * @return Paiement + */ + @GET + @Path("/{id}") + public Response trouverParId(@PathParam("id") UUID id) { + try { + PaiementDTO result = paiementService.trouverParId(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un paiement par son numéro de référence + * + * @param numeroReference Numéro de référence + * @return Paiement + */ + @GET + @Path("/reference/{numeroReference}") + public Response trouverParNumeroReference(@PathParam("numeroReference") String numeroReference) { + try { + PaiementDTO result = paiementService.trouverParNumeroReference(numeroReference); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Paiement non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche du paiement"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du paiement: " + e.getMessage())) + .build(); + } + } + + /** + * Liste tous les paiements d'un membre + * + * @param membreId ID du membre + * @return Liste des paiements + */ + @GET + @Path("/membre/{membreId}") + public Response listerParMembre(@PathParam("membreId") UUID membreId) { + try { + List result = paiementService.listerParMembre(membreId); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des paiements"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des paiements: " + e.getMessage())) + .build(); + } + } + + /** Classe interne pour les réponses d'erreur */ + public static class ErrorResponse { + public String error; + + public ErrorResponse(String error) { + this.error = error; + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/resource/PreferencesResource.java b/src/main/java/dev/lions/unionflow/server/resource/PreferencesResource.java new file mode 100644 index 0000000..eaada05 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/PreferencesResource.java @@ -0,0 +1,75 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.service.PreferencesNotificationService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.Map; +import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** Resource REST pour la gestion des préférences utilisateur */ +@Path("/api/preferences") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@ApplicationScoped +@Tag(name = "Préférences", description = "API de gestion des préférences utilisateur") +public class PreferencesResource { + + private static final Logger LOG = Logger.getLogger(PreferencesResource.class); + + @Inject PreferencesNotificationService preferencesService; + + @GET + @Path("/{utilisateurId}") + @RolesAllowed({"USER", "ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Obtenir les préférences d'un utilisateur") + @APIResponse(responseCode = "200", description = "Préférences récupérées avec succès") + public Response obtenirPreferences( + @PathParam("utilisateurId") UUID utilisateurId) { + LOG.infof("Récupération des préférences pour l'utilisateur %s", utilisateurId); + Map preferences = preferencesService.obtenirPreferences(utilisateurId); + return Response.ok(preferences).build(); + } + + @PUT + @Path("/{utilisateurId}") + @RolesAllowed({"USER", "ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Mettre à jour les préférences d'un utilisateur") + @APIResponse(responseCode = "204", description = "Préférences mises à jour avec succès") + public Response mettreAJourPreferences( + @PathParam("utilisateurId") UUID utilisateurId, Map preferences) { + LOG.infof("Mise à jour des préférences pour l'utilisateur %s", utilisateurId); + preferencesService.mettreAJourPreferences(utilisateurId, preferences); + return Response.noContent().build(); + } + + @POST + @Path("/{utilisateurId}/reinitialiser") + @RolesAllowed({"USER", "ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Réinitialiser les préférences d'un utilisateur") + @APIResponse(responseCode = "204", description = "Préférences réinitialisées avec succès") + public Response reinitialiserPreferences(@PathParam("utilisateurId") UUID utilisateurId) { + LOG.infof("Réinitialisation des préférences pour l'utilisateur %s", utilisateurId); + preferencesService.reinitialiserPreferences(utilisateurId); + return Response.noContent().build(); + } + + @GET + @Path("/{utilisateurId}/export") + @RolesAllowed({"USER", "ADMIN", "SUPER_ADMIN"}) + @Operation(summary = "Exporter les préférences d'un utilisateur") + @APIResponse(responseCode = "200", description = "Préférences exportées avec succès") + public Response exporterPreferences(@PathParam("utilisateurId") UUID utilisateurId) { + LOG.infof("Export des préférences pour l'utilisateur %s", utilisateurId); + Map export = preferencesService.exporterPreferences(utilisateurId); + return Response.ok(export).build(); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/resource/TypeOrganisationResource.java b/src/main/java/dev/lions/unionflow/server/resource/TypeOrganisationResource.java new file mode 100644 index 0000000..66f0703 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/TypeOrganisationResource.java @@ -0,0 +1,165 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.organisation.TypeOrganisationDTO; +import dev.lions.unionflow.server.service.TypeOrganisationService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.logging.Logger; + +/** + * Ressource REST pour la gestion du catalogue des types d'organisation. + */ +@Path("/api/types-organisations") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Types d'organisation", description = "Catalogue des types d'organisation") +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class TypeOrganisationResource { + + private static final Logger LOG = Logger.getLogger(TypeOrganisationResource.class); + + @Inject TypeOrganisationService service; + + /** Liste les types d'organisation. */ + @GET + @Operation( + summary = "Lister les types d'organisation", + description = "Récupère la liste des types d'organisation, optionnellement seulement actifs") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Liste des types récupérée avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = TypeOrganisationDTO.class))), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response listTypes( + @Parameter(description = "Limiter aux types actifs", example = "true") + @QueryParam("onlyActifs") + @DefaultValue("true") + String onlyActifs) { + // Parsing manuel pour éviter toute erreur de conversion JAX-RS (qui peut renvoyer une 400) + boolean actifsSeulement = !"false".equalsIgnoreCase(onlyActifs); + List types = service.listAll(actifsSeulement); + return Response.ok(types).build(); + } + + /** Crée un nouveau type d'organisation (réservé à l'administration). */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Operation( + summary = "Créer un type d'organisation", + description = "Crée un nouveau type dans le catalogue (code doit exister dans l'enum)") + @APIResponses({ + @APIResponse( + responseCode = "201", + description = "Type créé avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = TypeOrganisationDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response create(TypeOrganisationDTO dto) { + try { + TypeOrganisationDTO created = service.create(dto); + return Response.status(Response.Status.CREATED).entity(created).build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Erreur lors de la création du type d'organisation: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de la création du type d'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Met à jour un type. */ + @PUT + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/{id}") + @Operation( + summary = "Mettre à jour un type d'organisation", + description = "Met à jour un type existant (libellé, description, ordre, actif, code)") + @APIResponses({ + @APIResponse( + responseCode = "200", + description = "Type mis à jour avec succès", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = TypeOrganisationDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "404", description = "Type non trouvé"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response update(@PathParam("id") UUID id, TypeOrganisationDTO dto) { + try { + TypeOrganisationDTO updated = service.update(id, dto); + return Response.ok(updated).build(); + } catch (IllegalArgumentException e) { + LOG.warnf("Erreur lors de la mise à jour du type d'organisation: %s", e.getMessage()); + return Response.status(Response.Status.BAD_REQUEST) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de la mise à jour du type d'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } + + /** Désactive un type (soft delete). */ + @DELETE + @RolesAllowed({"ADMIN"}) + @Path("/{id}") + @Operation( + summary = "Désactiver un type d'organisation", + description = "Désactive un type dans le catalogue (soft delete)") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Type désactivé avec succès"), + @APIResponse(responseCode = "404", description = "Type non trouvé"), + @APIResponse(responseCode = "401", description = "Non authentifié"), + @APIResponse(responseCode = "403", description = "Non autorisé") + }) + public Response disable(@PathParam("id") UUID id) { + try { + service.disable(id); + return Response.noContent().build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(Map.of("error", e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de la désactivation du type d'organisation"); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(Map.of("error", "Erreur interne du serveur")) + .build(); + } + } +} + + diff --git a/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java b/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java new file mode 100644 index 0000000..63b9b4c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/resource/WaveResource.java @@ -0,0 +1,269 @@ +package dev.lions.unionflow.server.resource; + +import dev.lions.unionflow.server.api.dto.wave.CompteWaveDTO; +import dev.lions.unionflow.server.api.dto.wave.TransactionWaveDTO; +import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave; +import dev.lions.unionflow.server.service.WaveService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.List; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Resource REST pour l'intégration Wave Mobile Money + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@Path("/api/wave") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@RolesAllowed({"ADMIN", "MEMBRE", "USER"}) +public class WaveResource { + + private static final Logger LOG = Logger.getLogger(WaveResource.class); + + @Inject WaveService waveService; + + // ======================================== + // COMPTES WAVE + // ======================================== + + /** + * Crée un nouveau compte Wave + * + * @param compteWaveDTO DTO du compte à créer + * @return Compte créé + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/comptes") + public Response creerCompteWave(@Valid CompteWaveDTO compteWaveDTO) { + try { + CompteWaveDTO result = waveService.creerCompteWave(compteWaveDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création du compte Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Met à jour un compte Wave + * + * @param id ID du compte + * @param compteWaveDTO DTO avec les modifications + * @return Compte mis à jour + */ + @PUT + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/comptes/{id}") + public Response mettreAJourCompteWave(@PathParam("id") UUID id, @Valid CompteWaveDTO compteWaveDTO) { + try { + CompteWaveDTO result = waveService.mettreAJourCompteWave(id, compteWaveDTO); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Compte Wave non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la mise à jour du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la mise à jour du compte Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Vérifie un compte Wave + * + * @param id ID du compte + * @return Compte vérifié + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/comptes/{id}/verifier") + public Response verifierCompteWave(@PathParam("id") UUID id) { + try { + CompteWaveDTO result = waveService.verifierCompteWave(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Compte Wave non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la vérification du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la vérification du compte Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un compte Wave par son ID + * + * @param id ID du compte + * @return Compte Wave + */ + @GET + @Path("/comptes/{id}") + public Response trouverCompteWaveParId(@PathParam("id") UUID id) { + try { + CompteWaveDTO result = waveService.trouverCompteWaveParId(id); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Compte Wave non trouvé")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du compte Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve un compte Wave par numéro de téléphone + * + * @param numeroTelephone Numéro de téléphone + * @return Compte Wave ou null + */ + @GET + @Path("/comptes/telephone/{numeroTelephone}") + public Response trouverCompteWaveParTelephone(@PathParam("numeroTelephone") String numeroTelephone) { + try { + CompteWaveDTO result = waveService.trouverCompteWaveParTelephone(numeroTelephone); + if (result == null) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Compte Wave non trouvé")) + .build(); + } + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche du compte Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche du compte Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Liste tous les comptes Wave d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des comptes Wave + */ + @GET + @Path("/comptes/organisation/{organisationId}") + public Response listerComptesWaveParOrganisation(@PathParam("organisationId") UUID organisationId) { + try { + List result = waveService.listerComptesWaveParOrganisation(organisationId); + return Response.ok(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la liste des comptes Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la liste des comptes Wave: " + e.getMessage())) + .build(); + } + } + + // ======================================== + // TRANSACTIONS WAVE + // ======================================== + + /** + * Crée une nouvelle transaction Wave + * + * @param transactionWaveDTO DTO de la transaction à créer + * @return Transaction créée + */ + @POST + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/transactions") + public Response creerTransactionWave(@Valid TransactionWaveDTO transactionWaveDTO) { + try { + TransactionWaveDTO result = waveService.creerTransactionWave(transactionWaveDTO); + return Response.status(Response.Status.CREATED).entity(result).build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la création de la transaction Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la création de la transaction Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Met à jour le statut d'une transaction Wave + * + * @param waveTransactionId Identifiant Wave de la transaction + * @param statut Nouveau statut + * @return Transaction mise à jour + */ + @PUT + @RolesAllowed({"ADMIN", "MEMBRE"}) + @Path("/transactions/{waveTransactionId}/statut") + public Response mettreAJourStatutTransaction( + @PathParam("waveTransactionId") String waveTransactionId, StatutTransactionWave statut) { + try { + TransactionWaveDTO result = waveService.mettreAJourStatutTransaction(waveTransactionId, statut); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Transaction Wave non trouvée")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la mise à jour du statut de la transaction Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity( + new ErrorResponse( + "Erreur lors de la mise à jour du statut de la transaction Wave: " + e.getMessage())) + .build(); + } + } + + /** + * Trouve une transaction Wave par son identifiant Wave + * + * @param waveTransactionId Identifiant Wave + * @return Transaction Wave + */ + @GET + @Path("/transactions/{waveTransactionId}") + public Response trouverTransactionWaveParId(@PathParam("waveTransactionId") String waveTransactionId) { + try { + TransactionWaveDTO result = waveService.trouverTransactionWaveParId(waveTransactionId); + return Response.ok(result).build(); + } catch (jakarta.ws.rs.NotFoundException e) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Transaction Wave non trouvée")) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche de la transaction Wave"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse("Erreur lors de la recherche de la transaction Wave: " + e.getMessage())) + .build(); + } + } + + /** Classe interne pour les réponses d'erreur */ + public static class ErrorResponse { + public String error; + + public ErrorResponse(String error) { + this.error = error; + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/security/SecurityConfig.java b/src/main/java/dev/lions/unionflow/server/security/SecurityConfig.java new file mode 100644 index 0000000..bea4aa8 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/security/SecurityConfig.java @@ -0,0 +1,214 @@ +package dev.lions.unionflow.server.security; + +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.util.Set; +import org.jboss.logging.Logger; + +/** + * Configuration et utilitaires de sécurité avec Keycloak + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@ApplicationScoped +public class SecurityConfig { + + private static final Logger LOG = Logger.getLogger(SecurityConfig.class); + + @Inject KeycloakService keycloakService; + + /** Rôles disponibles dans l'application */ + public static class Roles { + public static final String ADMIN = "ADMIN"; + public static final String GESTIONNAIRE_MEMBRE = "GESTIONNAIRE_MEMBRE"; + public static final String TRESORIER = "TRESORIER"; + public static final String SECRETAIRE = "SECRETAIRE"; + public static final String MEMBRE = "MEMBRE"; + public static final String PRESIDENT = "PRESIDENT"; + public static final String VICE_PRESIDENT = "VICE_PRESIDENT"; + public static final String ORGANISATEUR_EVENEMENT = "ORGANISATEUR_EVENEMENT"; + public static final String GESTIONNAIRE_SOLIDARITE = "GESTIONNAIRE_SOLIDARITE"; + public static final String AUDITEUR = "AUDITEUR"; + } + + /** Permissions disponibles dans l'application */ + public static class Permissions { + // Permissions membres + public static final String CREATE_MEMBRE = "CREATE_MEMBRE"; + public static final String READ_MEMBRE = "READ_MEMBRE"; + public static final String UPDATE_MEMBRE = "UPDATE_MEMBRE"; + public static final String DELETE_MEMBRE = "DELETE_MEMBRE"; + + // Permissions organisations + public static final String CREATE_ORGANISATION = "CREATE_ORGANISATION"; + public static final String READ_ORGANISATION = "READ_ORGANISATION"; + public static final String UPDATE_ORGANISATION = "UPDATE_ORGANISATION"; + public static final String DELETE_ORGANISATION = "DELETE_ORGANISATION"; + + // Permissions événements + public static final String CREATE_EVENEMENT = "CREATE_EVENEMENT"; + public static final String READ_EVENEMENT = "READ_EVENEMENT"; + public static final String UPDATE_EVENEMENT = "UPDATE_EVENEMENT"; + public static final String DELETE_EVENEMENT = "DELETE_EVENEMENT"; + + // Permissions finances + public static final String CREATE_COTISATION = "CREATE_COTISATION"; + public static final String READ_COTISATION = "READ_COTISATION"; + public static final String UPDATE_COTISATION = "UPDATE_COTISATION"; + public static final String DELETE_COTISATION = "DELETE_COTISATION"; + + // Permissions solidarité + public static final String CREATE_SOLIDARITE = "CREATE_SOLIDARITE"; + public static final String READ_SOLIDARITE = "READ_SOLIDARITE"; + public static final String UPDATE_SOLIDARITE = "UPDATE_SOLIDARITE"; + public static final String DELETE_SOLIDARITE = "DELETE_SOLIDARITE"; + + // Permissions administration + public static final String ADMIN_USERS = "ADMIN_USERS"; + public static final String ADMIN_SYSTEM = "ADMIN_SYSTEM"; + public static final String VIEW_REPORTS = "VIEW_REPORTS"; + public static final String EXPORT_DATA = "EXPORT_DATA"; + } + + /** + * Vérifie si l'utilisateur actuel a un rôle spécifique + * + * @param role le rôle à vérifier + * @return true si l'utilisateur a le rôle + */ + public boolean hasRole(String role) { + return keycloakService.hasRole(role); + } + + /** + * Vérifie si l'utilisateur actuel a au moins un des rôles spécifiés + * + * @param roles les rôles à vérifier + * @return true si l'utilisateur a au moins un des rôles + */ + public boolean hasAnyRole(String... roles) { + return keycloakService.hasAnyRole(roles); + } + + /** + * Vérifie si l'utilisateur actuel a tous les rôles spécifiés + * + * @param roles les rôles à vérifier + * @return true si l'utilisateur a tous les rôles + */ + public boolean hasAllRoles(String... roles) { + return keycloakService.hasAllRoles(roles); + } + + /** + * Obtient l'ID de l'utilisateur actuel + * + * @return l'ID de l'utilisateur ou null si non authentifié + */ + public String getCurrentUserId() { + return keycloakService.getCurrentUserId(); + } + + /** + * Obtient l'email de l'utilisateur actuel + * + * @return l'email de l'utilisateur ou null si non authentifié + */ + public String getCurrentUserEmail() { + return keycloakService.getCurrentUserEmail(); + } + + /** + * Obtient tous les rôles de l'utilisateur actuel + * + * @return les rôles de l'utilisateur + */ + public Set getCurrentUserRoles() { + return keycloakService.getCurrentUserRoles(); + } + + /** + * Vérifie si l'utilisateur actuel est authentifié + * + * @return true si l'utilisateur est authentifié + */ + public boolean isAuthenticated() { + return keycloakService.isAuthenticated(); + } + + /** + * Vérifie si l'utilisateur actuel est un administrateur + * + * @return true si l'utilisateur est administrateur + */ + public boolean isAdmin() { + return hasRole(Roles.ADMIN); + } + + /** + * Vérifie si l'utilisateur actuel peut gérer les membres + * + * @return true si l'utilisateur peut gérer les membres + */ + public boolean canManageMembers() { + return hasAnyRole(Roles.ADMIN, Roles.GESTIONNAIRE_MEMBRE, Roles.PRESIDENT, Roles.SECRETAIRE); + } + + /** + * Vérifie si l'utilisateur actuel peut gérer les finances + * + * @return true si l'utilisateur peut gérer les finances + */ + public boolean canManageFinances() { + return hasAnyRole(Roles.ADMIN, Roles.TRESORIER, Roles.PRESIDENT); + } + + /** + * Vérifie si l'utilisateur actuel peut gérer les événements + * + * @return true si l'utilisateur peut gérer les événements + */ + public boolean canManageEvents() { + return hasAnyRole(Roles.ADMIN, Roles.ORGANISATEUR_EVENEMENT, Roles.PRESIDENT, Roles.SECRETAIRE); + } + + /** + * Vérifie si l'utilisateur actuel peut gérer les organisations + * + * @return true si l'utilisateur peut gérer les organisations + */ + public boolean canManageOrganizations() { + return hasAnyRole(Roles.ADMIN, Roles.PRESIDENT); + } + + /** + * Vérifie si l'utilisateur actuel peut accéder aux données d'un membre spécifique + * + * @param membreId l'ID du membre + * @return true si l'utilisateur peut accéder aux données + */ + public boolean canAccessMemberData(String membreId) { + // Un utilisateur peut toujours accéder à ses propres données + if (membreId.equals(getCurrentUserId())) { + return true; + } + + // Les gestionnaires peuvent accéder aux données de tous les membres + return canManageMembers(); + } + + /** Log les informations de sécurité pour debug */ + public void logSecurityInfo() { + if (LOG.isDebugEnabled()) { + if (isAuthenticated()) { + LOG.debugf( + "Utilisateur authentifié: %s, Rôles: %s", getCurrentUserEmail(), getCurrentUserRoles()); + } else { + LOG.debug("Utilisateur non authentifié"); + } + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/AdhesionService.java b/src/main/java/dev/lions/unionflow/server/service/AdhesionService.java new file mode 100644 index 0000000..55aa855 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/AdhesionService.java @@ -0,0 +1,559 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.finance.AdhesionDTO; +import dev.lions.unionflow.server.entity.Adhesion; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.AdhesionRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +/** + * Service métier pour la gestion des adhésions + * Contient la logique métier et les règles de validation + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@ApplicationScoped +@Slf4j +public class AdhesionService { + + @Inject AdhesionRepository adhesionRepository; + + @Inject MembreRepository membreRepository; + + @Inject OrganisationRepository organisationRepository; + + /** + * Récupère toutes les adhésions avec pagination + * + * @param page numéro de page (0-based) + * @param size taille de la page + * @return liste des adhésions converties en DTO + */ + public List getAllAdhesions(int page, int size) { + log.debug("Récupération des adhésions - page: {}, size: {}", page, size); + + jakarta.persistence.TypedQuery query = + adhesionRepository + .getEntityManager() + .createQuery( + "SELECT a FROM Adhesion a ORDER BY a.dateDemande DESC", Adhesion.class); + query.setFirstResult(page * size); + query.setMaxResults(size); + List adhesions = query.getResultList(); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère une adhésion par son ID + * + * @param id identifiant UUID de l'adhésion + * @return DTO de l'adhésion + * @throws NotFoundException si l'adhésion n'existe pas + */ + public AdhesionDTO getAdhesionById(@NotNull UUID id) { + log.debug("Récupération de l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + return convertToDTO(adhesion); + } + + /** + * Récupère une adhésion par son numéro de référence + * + * @param numeroReference numéro de référence unique + * @return DTO de l'adhésion + * @throws NotFoundException si l'adhésion n'existe pas + */ + public AdhesionDTO getAdhesionByReference(@NotNull String numeroReference) { + log.debug("Récupération de l'adhésion avec référence: {}", numeroReference); + + Adhesion adhesion = + adhesionRepository + .findByNumeroReference(numeroReference) + .orElseThrow( + () -> + new NotFoundException( + "Adhésion non trouvée avec la référence: " + numeroReference)); + + return convertToDTO(adhesion); + } + + /** + * Crée une nouvelle adhésion + * + * @param adhesionDTO données de l'adhésion à créer + * @return DTO de l'adhésion créée + */ + @Transactional + public AdhesionDTO createAdhesion(@Valid AdhesionDTO adhesionDTO) { + log.info( + "Création d'une nouvelle adhésion pour le membre: {} et l'organisation: {}", + adhesionDTO.getMembreId(), + adhesionDTO.getOrganisationId()); + + // Validation du membre + Membre membre = + membreRepository + .findByIdOptional(adhesionDTO.getMembreId()) + .orElseThrow( + () -> + new NotFoundException( + "Membre non trouvé avec l'ID: " + adhesionDTO.getMembreId())); + + // Validation de l'organisation + Organisation organisation = + organisationRepository + .findByIdOptional(adhesionDTO.getOrganisationId()) + .orElseThrow( + () -> + new NotFoundException( + "Organisation non trouvée avec l'ID: " + adhesionDTO.getOrganisationId())); + + // Conversion DTO vers entité + Adhesion adhesion = convertToEntity(adhesionDTO); + adhesion.setMembre(membre); + adhesion.setOrganisation(organisation); + + // Génération automatique du numéro de référence si absent + if (adhesion.getNumeroReference() == null || adhesion.getNumeroReference().isEmpty()) { + adhesion.setNumeroReference(genererNumeroReference()); + } + + // Initialisation par défaut + if (adhesion.getDateDemande() == null) { + adhesion.setDateDemande(LocalDate.now()); + } + if (adhesion.getStatut() == null || adhesion.getStatut().isEmpty()) { + adhesion.setStatut("EN_ATTENTE"); + } + if (adhesion.getMontantPaye() == null) { + adhesion.setMontantPaye(BigDecimal.ZERO); + } + if (adhesion.getCodeDevise() == null || adhesion.getCodeDevise().isEmpty()) { + adhesion.setCodeDevise("XOF"); + } + + // Persistance + adhesionRepository.persist(adhesion); + + log.info( + "Adhésion créée avec succès - ID: {}, Référence: {}", + adhesion.getId(), + adhesion.getNumeroReference()); + + return convertToDTO(adhesion); + } + + /** + * Met à jour une adhésion existante + * + * @param id identifiant UUID de l'adhésion + * @param adhesionDTO nouvelles données + * @return DTO de l'adhésion mise à jour + */ + @Transactional + public AdhesionDTO updateAdhesion(@NotNull UUID id, @Valid AdhesionDTO adhesionDTO) { + log.info("Mise à jour de l'adhésion avec ID: {}", id); + + Adhesion adhesionExistante = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + // Mise à jour des champs modifiables + updateAdhesionFields(adhesionExistante, adhesionDTO); + + log.info("Adhésion mise à jour avec succès - ID: {}", id); + + return convertToDTO(adhesionExistante); + } + + /** + * Supprime (désactive) une adhésion + * + * @param id identifiant UUID de l'adhésion + */ + @Transactional + public void deleteAdhesion(@NotNull UUID id) { + log.info("Suppression de l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + // Vérification si l'adhésion peut être supprimée + if ("PAYEE".equals(adhesion.getStatut())) { + throw new IllegalStateException("Impossible de supprimer une adhésion déjà payée"); + } + + adhesion.setStatut("ANNULEE"); + + log.info("Adhésion supprimée avec succès - ID: {}", id); + } + + /** + * Approuve une adhésion + * + * @param id identifiant UUID de l'adhésion + * @param approuvePar nom de l'utilisateur qui approuve + * @return DTO de l'adhésion approuvée + */ + @Transactional + public AdhesionDTO approuverAdhesion(@NotNull UUID id, String approuvePar) { + log.info("Approbation de l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + if (!"EN_ATTENTE".equals(adhesion.getStatut())) { + throw new IllegalStateException( + "Seules les adhésions en attente peuvent être approuvées"); + } + + adhesion.setStatut("APPROUVEE"); + adhesion.setDateApprobation(LocalDate.now()); + adhesion.setApprouvePar(approuvePar); + adhesion.setDateValidation(LocalDate.now()); + + log.info("Adhésion approuvée avec succès - ID: {}", id); + + return convertToDTO(adhesion); + } + + /** + * Rejette une adhésion + * + * @param id identifiant UUID de l'adhésion + * @param motifRejet motif du rejet + * @return DTO de l'adhésion rejetée + */ + @Transactional + public AdhesionDTO rejeterAdhesion(@NotNull UUID id, String motifRejet) { + log.info("Rejet de l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + if (!"EN_ATTENTE".equals(adhesion.getStatut())) { + throw new IllegalStateException("Seules les adhésions en attente peuvent être rejetées"); + } + + adhesion.setStatut("REJETEE"); + adhesion.setMotifRejet(motifRejet); + + log.info("Adhésion rejetée avec succès - ID: {}", id); + + return convertToDTO(adhesion); + } + + /** + * Enregistre un paiement pour une adhésion + * + * @param id identifiant UUID de l'adhésion + * @param montantPaye montant payé + * @param methodePaiement méthode de paiement + * @param referencePaiement référence du paiement + * @return DTO de l'adhésion mise à jour + */ + @Transactional + public AdhesionDTO enregistrerPaiement( + @NotNull UUID id, + BigDecimal montantPaye, + String methodePaiement, + String referencePaiement) { + log.info("Enregistrement du paiement pour l'adhésion avec ID: {}", id); + + Adhesion adhesion = + adhesionRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id)); + + if (!"APPROUVEE".equals(adhesion.getStatut()) && !"EN_PAIEMENT".equals(adhesion.getStatut())) { + throw new IllegalStateException( + "Seules les adhésions approuvées peuvent recevoir un paiement"); + } + + BigDecimal nouveauMontantPaye = + adhesion.getMontantPaye() != null + ? adhesion.getMontantPaye().add(montantPaye) + : montantPaye; + + adhesion.setMontantPaye(nouveauMontantPaye); + adhesion.setMethodePaiement(methodePaiement); + adhesion.setReferencePaiement(referencePaiement); + adhesion.setDatePaiement(java.time.LocalDateTime.now()); + + // Mise à jour du statut si payée intégralement + if (adhesion.isPayeeIntegralement()) { + adhesion.setStatut("PAYEE"); + } else { + adhesion.setStatut("EN_PAIEMENT"); + } + + log.info("Paiement enregistré avec succès pour l'adhésion - ID: {}", id); + + return convertToDTO(adhesion); + } + + /** + * Récupère les adhésions d'un membre + * + * @param membreId identifiant UUID du membre + * @param page numéro de page + * @param size taille de la page + * @return liste des adhésions du membre + */ + public List getAdhesionsByMembre(@NotNull UUID membreId, int page, int size) { + log.debug("Récupération des adhésions du membre: {}", membreId); + + if (!membreRepository.findByIdOptional(membreId).isPresent()) { + throw new NotFoundException("Membre non trouvé avec l'ID: " + membreId); + } + + List adhesions = + adhesionRepository.findByMembreId(membreId).stream() + .skip(page * size) + .limit(size) + .collect(Collectors.toList()); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les adhésions d'une organisation + * + * @param organisationId identifiant UUID de l'organisation + * @param page numéro de page + * @param size taille de la page + * @return liste des adhésions de l'organisation + */ + public List getAdhesionsByOrganisation( + @NotNull UUID organisationId, int page, int size) { + log.debug("Récupération des adhésions de l'organisation: {}", organisationId); + + if (!organisationRepository.findByIdOptional(organisationId).isPresent()) { + throw new NotFoundException("Organisation non trouvée avec l'ID: " + organisationId); + } + + List adhesions = + adhesionRepository.findByOrganisationId(organisationId).stream() + .skip(page * size) + .limit(size) + .collect(Collectors.toList()); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les adhésions par statut + * + * @param statut statut recherché + * @param page numéro de page + * @param size taille de la page + * @return liste des adhésions avec le statut spécifié + */ + public List getAdhesionsByStatut(@NotNull String statut, int page, int size) { + log.debug("Récupération des adhésions avec statut: {}", statut); + + List adhesions = + adhesionRepository.findByStatut(statut).stream() + .skip(page * size) + .limit(size) + .collect(Collectors.toList()); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les adhésions en attente + * + * @param page numéro de page + * @param size taille de la page + * @return liste des adhésions en attente + */ + public List getAdhesionsEnAttente(int page, int size) { + log.debug("Récupération des adhésions en attente"); + + List adhesions = + adhesionRepository.findEnAttente().stream() + .skip(page * size) + .limit(size) + .collect(Collectors.toList()); + + return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les statistiques des adhésions + * + * @return map contenant les statistiques + */ + public Map getStatistiquesAdhesions() { + log.debug("Calcul des statistiques des adhésions"); + + long totalAdhesions = adhesionRepository.count(); + long adhesionsApprouvees = adhesionRepository.findByStatut("APPROUVEE").size(); + long adhesionsEnAttente = adhesionRepository.findEnAttente().size(); + long adhesionsPayees = adhesionRepository.findByStatut("PAYEE").size(); + + return Map.of( + "totalAdhesions", totalAdhesions, + "adhesionsApprouvees", adhesionsApprouvees, + "adhesionsEnAttente", adhesionsEnAttente, + "adhesionsPayees", adhesionsPayees, + "tauxApprobation", + totalAdhesions > 0 ? (adhesionsApprouvees * 100.0 / totalAdhesions) : 0.0, + "tauxPaiement", + adhesionsApprouvees > 0 + ? (adhesionsPayees * 100.0 / adhesionsApprouvees) + : 0.0); + } + + /** Génère un numéro de référence unique pour une adhésion */ + private String genererNumeroReference() { + return "ADH-" + System.currentTimeMillis() + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + } + + /** Convertit une entité Adhesion en DTO */ + private AdhesionDTO convertToDTO(Adhesion adhesion) { + if (adhesion == null) { + return null; + } + + AdhesionDTO dto = new AdhesionDTO(); + + dto.setId(adhesion.getId()); + dto.setNumeroReference(adhesion.getNumeroReference()); + + // Conversion du membre associé + if (adhesion.getMembre() != null) { + dto.setMembreId(adhesion.getMembre().getId()); + dto.setNomMembre(adhesion.getMembre().getNomComplet()); + dto.setNumeroMembre(adhesion.getMembre().getNumeroMembre()); + dto.setEmailMembre(adhesion.getMembre().getEmail()); + } + + // Conversion de l'organisation + if (adhesion.getOrganisation() != null) { + dto.setOrganisationId(adhesion.getOrganisation().getId()); + dto.setNomOrganisation(adhesion.getOrganisation().getNom()); + } + + // Propriétés de l'adhésion + dto.setDateDemande(adhesion.getDateDemande()); + dto.setFraisAdhesion(adhesion.getFraisAdhesion()); + dto.setMontantPaye(adhesion.getMontantPaye()); + dto.setCodeDevise(adhesion.getCodeDevise()); + dto.setStatut(adhesion.getStatut()); + dto.setDateApprobation(adhesion.getDateApprobation()); + dto.setDatePaiement(adhesion.getDatePaiement()); + dto.setMethodePaiement(adhesion.getMethodePaiement()); + dto.setReferencePaiement(adhesion.getReferencePaiement()); + dto.setMotifRejet(adhesion.getMotifRejet()); + dto.setObservations(adhesion.getObservations()); + dto.setApprouvePar(adhesion.getApprouvePar()); + dto.setDateValidation(adhesion.getDateValidation()); + + // Métadonnées de BaseEntity + dto.setDateCreation(adhesion.getDateCreation()); + dto.setDateModification(adhesion.getDateModification()); + dto.setCreePar(adhesion.getCreePar()); + dto.setModifiePar(adhesion.getModifiePar()); + dto.setActif(adhesion.getActif()); + + return dto; + } + + /** Convertit un DTO en entité Adhesion */ + private Adhesion convertToEntity(AdhesionDTO dto) { + if (dto == null) { + return null; + } + + Adhesion adhesion = new Adhesion(); + + adhesion.setNumeroReference(dto.getNumeroReference()); + adhesion.setDateDemande(dto.getDateDemande()); + adhesion.setFraisAdhesion(dto.getFraisAdhesion()); + adhesion.setMontantPaye(dto.getMontantPaye() != null ? dto.getMontantPaye() : BigDecimal.ZERO); + adhesion.setCodeDevise(dto.getCodeDevise()); + adhesion.setStatut(dto.getStatut()); + adhesion.setDateApprobation(dto.getDateApprobation()); + adhesion.setDatePaiement(dto.getDatePaiement()); + adhesion.setMethodePaiement(dto.getMethodePaiement()); + adhesion.setReferencePaiement(dto.getReferencePaiement()); + adhesion.setMotifRejet(dto.getMotifRejet()); + adhesion.setObservations(dto.getObservations()); + adhesion.setApprouvePar(dto.getApprouvePar()); + adhesion.setDateValidation(dto.getDateValidation()); + + return adhesion; + } + + /** Met à jour les champs modifiables d'une adhésion existante */ + private void updateAdhesionFields(Adhesion adhesion, AdhesionDTO dto) { + if (dto.getFraisAdhesion() != null) { + adhesion.setFraisAdhesion(dto.getFraisAdhesion()); + } + if (dto.getMontantPaye() != null) { + adhesion.setMontantPaye(dto.getMontantPaye()); + } + if (dto.getStatut() != null) { + adhesion.setStatut(dto.getStatut()); + } + if (dto.getDateApprobation() != null) { + adhesion.setDateApprobation(dto.getDateApprobation()); + } + if (dto.getDatePaiement() != null) { + adhesion.setDatePaiement(dto.getDatePaiement()); + } + if (dto.getMethodePaiement() != null) { + adhesion.setMethodePaiement(dto.getMethodePaiement()); + } + if (dto.getReferencePaiement() != null) { + adhesion.setReferencePaiement(dto.getReferencePaiement()); + } + if (dto.getMotifRejet() != null) { + adhesion.setMotifRejet(dto.getMotifRejet()); + } + if (dto.getObservations() != null) { + adhesion.setObservations(dto.getObservations()); + } + if (dto.getApprouvePar() != null) { + adhesion.setApprouvePar(dto.getApprouvePar()); + } + if (dto.getDateValidation() != null) { + adhesion.setDateValidation(dto.getDateValidation()); + } + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/service/AdresseService.java b/src/main/java/dev/lions/unionflow/server/service/AdresseService.java new file mode 100644 index 0000000..ddcacc9 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/AdresseService.java @@ -0,0 +1,353 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.adresse.AdresseDTO; +import dev.lions.unionflow.server.api.enums.adresse.TypeAdresse; +import dev.lions.unionflow.server.entity.Adresse; +import dev.lions.unionflow.server.entity.Evenement; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.AdresseRepository; +import dev.lions.unionflow.server.repository.EvenementRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** + * Service métier pour la gestion des adresses + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class AdresseService { + + private static final Logger LOG = Logger.getLogger(AdresseService.class); + + @Inject AdresseRepository adresseRepository; + + @Inject OrganisationRepository organisationRepository; + + @Inject MembreRepository membreRepository; + + @Inject EvenementRepository evenementRepository; + + /** + * Crée une nouvelle adresse + * + * @param adresseDTO DTO de l'adresse à créer + * @return DTO de l'adresse créée + */ + @Transactional + public AdresseDTO creerAdresse(AdresseDTO adresseDTO) { + LOG.infof("Création d'une nouvelle adresse de type: %s", adresseDTO.getTypeAdresse()); + + Adresse adresse = convertToEntity(adresseDTO); + + // Gestion de l'adresse principale + if (Boolean.TRUE.equals(adresseDTO.getPrincipale())) { + desactiverAutresPrincipales(adresseDTO); + } + + adresseRepository.persist(adresse); + LOG.infof("Adresse créée avec succès: ID=%s", adresse.getId()); + + return convertToDTO(adresse); + } + + /** + * Met à jour une adresse existante + * + * @param id ID de l'adresse + * @param adresseDTO DTO avec les nouvelles données + * @return DTO de l'adresse mise à jour + */ + @Transactional + public AdresseDTO mettreAJourAdresse(UUID id, AdresseDTO adresseDTO) { + LOG.infof("Mise à jour de l'adresse ID: %s", id); + + Adresse adresse = + adresseRepository + .findAdresseById(id) + .orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id)); + + // Mise à jour des champs + updateFromDTO(adresse, adresseDTO); + + // Gestion de l'adresse principale + if (Boolean.TRUE.equals(adresseDTO.getPrincipale())) { + desactiverAutresPrincipales(adresseDTO); + } + + adresseRepository.persist(adresse); + LOG.infof("Adresse mise à jour avec succès: ID=%s", id); + + return convertToDTO(adresse); + } + + /** + * Supprime une adresse + * + * @param id ID de l'adresse + */ + @Transactional + public void supprimerAdresse(UUID id) { + LOG.infof("Suppression de l'adresse ID: %s", id); + + Adresse adresse = + adresseRepository + .findAdresseById(id) + .orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id)); + + adresseRepository.delete(adresse); + LOG.infof("Adresse supprimée avec succès: ID=%s", id); + } + + /** + * Trouve une adresse par son ID + * + * @param id ID de l'adresse + * @return DTO de l'adresse + */ + public AdresseDTO trouverParId(UUID id) { + return adresseRepository + .findAdresseById(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id)); + } + + /** + * Trouve toutes les adresses d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des adresses + */ + public List trouverParOrganisation(UUID organisationId) { + return adresseRepository.findByOrganisationId(organisationId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Trouve toutes les adresses d'un membre + * + * @param membreId ID du membre + * @return Liste des adresses + */ + public List trouverParMembre(UUID membreId) { + return adresseRepository.findByMembreId(membreId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Trouve l'adresse d'un événement + * + * @param evenementId ID de l'événement + * @return DTO de l'adresse ou null + */ + public AdresseDTO trouverParEvenement(UUID evenementId) { + return adresseRepository + .findByEvenementId(evenementId) + .map(this::convertToDTO) + .orElse(null); + } + + /** + * Trouve l'adresse principale d'une organisation + * + * @param organisationId ID de l'organisation + * @return DTO de l'adresse principale ou null + */ + public AdresseDTO trouverPrincipaleParOrganisation(UUID organisationId) { + return adresseRepository + .findPrincipaleByOrganisationId(organisationId) + .map(this::convertToDTO) + .orElse(null); + } + + /** + * Trouve l'adresse principale d'un membre + * + * @param membreId ID du membre + * @return DTO de l'adresse principale ou null + */ + public AdresseDTO trouverPrincipaleParMembre(UUID membreId) { + return adresseRepository + .findPrincipaleByMembreId(membreId) + .map(this::convertToDTO) + .orElse(null); + } + + // ======================================== + // MÉTHODES PRIVÉES + // ======================================== + + /** Désactive les autres adresses principales pour la même entité */ + private void desactiverAutresPrincipales(AdresseDTO adresseDTO) { + List autresPrincipales; + + if (adresseDTO.getOrganisationId() != null) { + autresPrincipales = + adresseRepository + .find("organisation.id = ?1 AND principale = true", adresseDTO.getOrganisationId()) + .list(); + } else if (adresseDTO.getMembreId() != null) { + autresPrincipales = + adresseRepository + .find("membre.id = ?1 AND principale = true", adresseDTO.getMembreId()) + .list(); + } else { + return; // Pas d'entité associée + } + + autresPrincipales.forEach(adr -> adr.setPrincipale(false)); + } + + /** Convertit une entité en DTO */ + private AdresseDTO convertToDTO(Adresse adresse) { + if (adresse == null) { + return null; + } + + AdresseDTO dto = new AdresseDTO(); + dto.setId(adresse.getId()); + dto.setTypeAdresse(convertTypeAdresse(adresse.getTypeAdresse())); + dto.setAdresse(adresse.getAdresse()); + dto.setComplementAdresse(adresse.getComplementAdresse()); + dto.setCodePostal(adresse.getCodePostal()); + dto.setVille(adresse.getVille()); + dto.setRegion(adresse.getRegion()); + dto.setPays(adresse.getPays()); + dto.setLatitude(adresse.getLatitude()); + dto.setLongitude(adresse.getLongitude()); + dto.setPrincipale(adresse.getPrincipale()); + dto.setLibelle(adresse.getLibelle()); + dto.setNotes(adresse.getNotes()); + + if (adresse.getOrganisation() != null) { + dto.setOrganisationId(adresse.getOrganisation().getId()); + } + if (adresse.getMembre() != null) { + dto.setMembreId(adresse.getMembre().getId()); + } + if (adresse.getEvenement() != null) { + dto.setEvenementId(adresse.getEvenement().getId()); + } + + dto.setAdresseComplete(adresse.getAdresseComplete()); + dto.setDateCreation(adresse.getDateCreation()); + dto.setDateModification(adresse.getDateModification()); + dto.setActif(adresse.getActif()); + + return dto; + } + + /** Convertit un DTO en entité */ + private Adresse convertToEntity(AdresseDTO dto) { + if (dto == null) { + return null; + } + + Adresse adresse = new Adresse(); + adresse.setTypeAdresse(convertTypeAdresse(dto.getTypeAdresse())); + adresse.setAdresse(dto.getAdresse()); + adresse.setComplementAdresse(dto.getComplementAdresse()); + adresse.setCodePostal(dto.getCodePostal()); + adresse.setVille(dto.getVille()); + adresse.setRegion(dto.getRegion()); + adresse.setPays(dto.getPays()); + adresse.setLatitude(dto.getLatitude()); + adresse.setLongitude(dto.getLongitude()); + adresse.setPrincipale(dto.getPrincipale() != null ? dto.getPrincipale() : false); + adresse.setLibelle(dto.getLibelle()); + adresse.setNotes(dto.getNotes()); + + // Relations + if (dto.getOrganisationId() != null) { + Organisation org = + organisationRepository + .findByIdOptional(dto.getOrganisationId()) + .orElseThrow( + () -> + new NotFoundException( + "Organisation non trouvée avec l'ID: " + dto.getOrganisationId())); + adresse.setOrganisation(org); + } + + if (dto.getMembreId() != null) { + Membre membre = + membreRepository + .findByIdOptional(dto.getMembreId()) + .orElseThrow( + () -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId())); + adresse.setMembre(membre); + } + + if (dto.getEvenementId() != null) { + Evenement evenement = + evenementRepository + .findByIdOptional(dto.getEvenementId()) + .orElseThrow( + () -> + new NotFoundException( + "Événement non trouvé avec l'ID: " + dto.getEvenementId())); + adresse.setEvenement(evenement); + } + + return adresse; + } + + /** Met à jour une entité à partir d'un DTO */ + private void updateFromDTO(Adresse adresse, AdresseDTO dto) { + if (dto.getTypeAdresse() != null) { + adresse.setTypeAdresse(convertTypeAdresse(dto.getTypeAdresse())); + } + if (dto.getAdresse() != null) { + adresse.setAdresse(dto.getAdresse()); + } + if (dto.getComplementAdresse() != null) { + adresse.setComplementAdresse(dto.getComplementAdresse()); + } + if (dto.getCodePostal() != null) { + adresse.setCodePostal(dto.getCodePostal()); + } + if (dto.getVille() != null) { + adresse.setVille(dto.getVille()); + } + if (dto.getRegion() != null) { + adresse.setRegion(dto.getRegion()); + } + if (dto.getPays() != null) { + adresse.setPays(dto.getPays()); + } + if (dto.getLatitude() != null) { + adresse.setLatitude(dto.getLatitude()); + } + if (dto.getLongitude() != null) { + adresse.setLongitude(dto.getLongitude()); + } + if (dto.getPrincipale() != null) { + adresse.setPrincipale(dto.getPrincipale()); + } + if (dto.getLibelle() != null) { + adresse.setLibelle(dto.getLibelle()); + } + if (dto.getNotes() != null) { + adresse.setNotes(dto.getNotes()); + } + } + + /** Convertit TypeAdresse (entité) vers TypeAdresse (DTO) - même enum, pas de conversion nécessaire */ + private TypeAdresse convertTypeAdresse(TypeAdresse type) { + return type != null ? type : TypeAdresse.AUTRE; // Même enum, valeur par défaut si null + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/service/AnalyticsService.java b/src/main/java/dev/lions/unionflow/server/service/AnalyticsService.java new file mode 100644 index 0000000..3535da0 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/AnalyticsService.java @@ -0,0 +1,478 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.analytics.AnalyticsDataDTO; +import dev.lions.unionflow.server.api.dto.analytics.DashboardWidgetDTO; +import dev.lions.unionflow.server.api.dto.analytics.KPITrendDTO; +import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse; +// import dev.lions.unionflow.server.entity.DemandeAide; +import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique; +import dev.lions.unionflow.server.repository.CotisationRepository; +import dev.lions.unionflow.server.repository.DemandeAideRepository; +import dev.lions.unionflow.server.repository.EvenementRepository; +// import dev.lions.unionflow.server.repository.DemandeAideRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; + +/** + * Service principal pour les analytics et métriques UnionFlow + * + *

Ce service calcule et fournit toutes les métriques analytics pour les tableaux de bord, + * rapports et widgets. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-16 + */ +@ApplicationScoped +@Slf4j +public class AnalyticsService { + + @Inject OrganisationRepository organisationRepository; + + @Inject MembreRepository membreRepository; + + @Inject CotisationRepository cotisationRepository; + + @Inject DemandeAideRepository demandeAideRepository; + + @Inject EvenementRepository evenementRepository; + + // @Inject + // DemandeAideRepository demandeAideRepository; + + @Inject KPICalculatorService kpiCalculatorService; + + @Inject TrendAnalysisService trendAnalysisService; + + /** + * Calcule une métrique analytics pour une période donnée + * + * @param typeMetrique Le type de métrique à calculer + * @param periodeAnalyse La période d'analyse + * @param organisationId L'ID de l'organisation (optionnel) + * @return Les données analytics calculées + */ + @Transactional + public AnalyticsDataDTO calculerMetrique( + TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) { + log.info( + "Calcul de la métrique {} pour la période {} et l'organisation {}", + typeMetrique, + periodeAnalyse, + organisationId); + + LocalDateTime dateDebut = periodeAnalyse.getDateDebut(); + LocalDateTime dateFin = periodeAnalyse.getDateFin(); + + BigDecimal valeur = + switch (typeMetrique) { + // Métriques membres + case NOMBRE_MEMBRES_ACTIFS -> + calculerNombreMembresActifs(organisationId, dateDebut, dateFin); + case NOMBRE_MEMBRES_INACTIFS -> + calculerNombreMembresInactifs(organisationId, dateDebut, dateFin); + case TAUX_CROISSANCE_MEMBRES -> + calculerTauxCroissanceMembres(organisationId, dateDebut, dateFin); + case MOYENNE_AGE_MEMBRES -> calculerMoyenneAgeMembres(organisationId, dateDebut, dateFin); + + // Métriques financières + case TOTAL_COTISATIONS_COLLECTEES -> + calculerTotalCotisationsCollectees(organisationId, dateDebut, dateFin); + case COTISATIONS_EN_ATTENTE -> + calculerCotisationsEnAttente(organisationId, dateDebut, dateFin); + case TAUX_RECOUVREMENT_COTISATIONS -> + calculerTauxRecouvrementCotisations(organisationId, dateDebut, dateFin); + case MOYENNE_COTISATION_MEMBRE -> + calculerMoyenneCotisationMembre(organisationId, dateDebut, dateFin); + + // Métriques événements + case NOMBRE_EVENEMENTS_ORGANISES -> + calculerNombreEvenementsOrganises(organisationId, dateDebut, dateFin); + case TAUX_PARTICIPATION_EVENEMENTS -> + calculerTauxParticipationEvenements(organisationId, dateDebut, dateFin); + case MOYENNE_PARTICIPANTS_EVENEMENT -> + calculerMoyenneParticipantsEvenement(organisationId, dateDebut, dateFin); + + // Métriques solidarité + case NOMBRE_DEMANDES_AIDE -> + calculerNombreDemandesAide(organisationId, dateDebut, dateFin); + case MONTANT_AIDES_ACCORDEES -> + calculerMontantAidesAccordees(organisationId, dateDebut, dateFin); + case TAUX_APPROBATION_AIDES -> + calculerTauxApprobationAides(organisationId, dateDebut, dateFin); + + default -> BigDecimal.ZERO; + }; + + // Calcul de la valeur précédente pour comparaison + BigDecimal valeurPrecedente = + calculerValeurPrecedente(typeMetrique, periodeAnalyse, organisationId); + BigDecimal pourcentageEvolution = calculerPourcentageEvolution(valeur, valeurPrecedente); + + return AnalyticsDataDTO.builder() + .typeMetrique(typeMetrique) + .periodeAnalyse(periodeAnalyse) + .valeur(valeur) + .valeurPrecedente(valeurPrecedente) + .pourcentageEvolution(pourcentageEvolution) + .dateDebut(dateDebut) + .dateFin(dateFin) + .dateCalcul(LocalDateTime.now()) + .organisationId(organisationId) + .nomOrganisation(obtenirNomOrganisation(organisationId)) + .indicateurFiabilite(new BigDecimal("95.0")) + .niveauPriorite(3) + .tempsReel(false) + .necessiteMiseAJour(false) + .build(); + } + + /** + * Calcule les tendances d'un KPI sur une période + * + * @param typeMetrique Le type de métrique + * @param periodeAnalyse La période d'analyse + * @param organisationId L'ID de l'organisation (optionnel) + * @return Les données de tendance du KPI + */ + @Transactional + public KPITrendDTO calculerTendanceKPI( + TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) { + log.info( + "Calcul de la tendance KPI {} pour la période {} et l'organisation {}", + typeMetrique, + periodeAnalyse, + organisationId); + + return trendAnalysisService.calculerTendance(typeMetrique, periodeAnalyse, organisationId); + } + + /** + * Obtient les métriques pour un tableau de bord + * + * @param organisationId L'ID de l'organisation + * @param utilisateurId L'ID de l'utilisateur + * @return La liste des widgets du tableau de bord + */ + @Transactional + public List obtenirMetriquesTableauBord( + UUID organisationId, UUID utilisateurId) { + log.info( + "Obtention des métriques du tableau de bord pour l'organisation {} et l'utilisateur {}", + organisationId, + utilisateurId); + + List widgets = new ArrayList<>(); + + // Widget KPI Membres Actifs + widgets.add( + creerWidgetKPI( + TypeMetrique.NOMBRE_MEMBRES_ACTIFS, + PeriodeAnalyse.CE_MOIS, + organisationId, + utilisateurId, + 0, + 0, + 3, + 2)); + + // Widget KPI Cotisations + widgets.add( + creerWidgetKPI( + TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, + PeriodeAnalyse.CE_MOIS, + organisationId, + utilisateurId, + 3, + 0, + 3, + 2)); + + // Widget KPI Événements + widgets.add( + creerWidgetKPI( + TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, + PeriodeAnalyse.CE_MOIS, + organisationId, + utilisateurId, + 6, + 0, + 3, + 2)); + + // Widget KPI Solidarité + widgets.add( + creerWidgetKPI( + TypeMetrique.NOMBRE_DEMANDES_AIDE, + PeriodeAnalyse.CE_MOIS, + organisationId, + utilisateurId, + 9, + 0, + 3, + 2)); + + // Widget Graphique Évolution Membres + widgets.add( + creerWidgetGraphique( + TypeMetrique.NOMBRE_MEMBRES_ACTIFS, + PeriodeAnalyse.SIX_DERNIERS_MOIS, + organisationId, + utilisateurId, + 0, + 2, + 6, + 4, + "line")); + + // Widget Graphique Évolution Financière + widgets.add( + creerWidgetGraphique( + TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, + PeriodeAnalyse.SIX_DERNIERS_MOIS, + organisationId, + utilisateurId, + 6, + 2, + 6, + 4, + "area")); + + return widgets; + } + + // === MÉTHODES PRIVÉES DE CALCUL === + + private BigDecimal calculerNombreMembresActifs( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long count = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin); + return new BigDecimal(count); + } + + private BigDecimal calculerNombreMembresInactifs( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long count = membreRepository.countMembresInactifs(organisationId, dateDebut, dateFin); + return new BigDecimal(count); + } + + private BigDecimal calculerTauxCroissanceMembres( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long membresActuels = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin); + Long membresPrecedents = + membreRepository.countMembresActifs( + organisationId, dateDebut.minusMonths(1), dateFin.minusMonths(1)); + + if (membresPrecedents == 0) return BigDecimal.ZERO; + + BigDecimal croissance = + new BigDecimal(membresActuels - membresPrecedents) + .divide(new BigDecimal(membresPrecedents), 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + + return croissance; + } + + private BigDecimal calculerMoyenneAgeMembres( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Double moyenneAge = membreRepository.calculerMoyenneAge(organisationId, dateDebut, dateFin); + return moyenneAge != null + ? new BigDecimal(moyenneAge).setScale(1, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + } + + private BigDecimal calculerTotalCotisationsCollectees( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal total = cotisationRepository.sumMontantsPayes(organisationId, dateDebut, dateFin); + return total != null ? total : BigDecimal.ZERO; + } + + private BigDecimal calculerCotisationsEnAttente( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal total = + cotisationRepository.sumMontantsEnAttente(organisationId, dateDebut, dateFin); + return total != null ? total : BigDecimal.ZERO; + } + + private BigDecimal calculerTauxRecouvrementCotisations( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal collectees = calculerTotalCotisationsCollectees(organisationId, dateDebut, dateFin); + BigDecimal enAttente = calculerCotisationsEnAttente(organisationId, dateDebut, dateFin); + BigDecimal total = collectees.add(enAttente); + + if (total.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO; + + return collectees.divide(total, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100")); + } + + private BigDecimal calculerMoyenneCotisationMembre( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal total = calculerTotalCotisationsCollectees(organisationId, dateDebut, dateFin); + Long nombreMembres = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin); + + if (nombreMembres == 0) return BigDecimal.ZERO; + + return total.divide(new BigDecimal(nombreMembres), 2, RoundingMode.HALF_UP); + } + + private BigDecimal calculerNombreEvenementsOrganises( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long count = evenementRepository.countEvenements(organisationId, dateDebut, dateFin); + return new BigDecimal(count); + } + + private BigDecimal calculerTauxParticipationEvenements( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + // Implémentation simplifiée - à enrichir selon les besoins + return new BigDecimal("75.5"); // Valeur par défaut + } + + private BigDecimal calculerMoyenneParticipantsEvenement( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Double moyenne = + evenementRepository.calculerMoyenneParticipants(organisationId, dateDebut, dateFin); + return moyenne != null + ? new BigDecimal(moyenne).setScale(1, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + } + + private BigDecimal calculerNombreDemandesAide( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long count = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin); + return new BigDecimal(count); + } + + private BigDecimal calculerMontantAidesAccordees( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal total = + demandeAideRepository.sumMontantsAccordes(organisationId, dateDebut, dateFin); + return total != null ? total : BigDecimal.ZERO; + } + + private BigDecimal calculerTauxApprobationAides( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long totalDemandes = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin); + Long demandesApprouvees = + demandeAideRepository.countDemandesApprouvees(organisationId, dateDebut, dateFin); + + if (totalDemandes == 0) return BigDecimal.ZERO; + + return new BigDecimal(demandesApprouvees) + .divide(new BigDecimal(totalDemandes), 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + } + + private BigDecimal calculerValeurPrecedente( + TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) { + // Calcul de la période précédente + LocalDateTime dateDebutPrecedente = + periodeAnalyse.getDateDebut().minus(periodeAnalyse.getDuree(), periodeAnalyse.getUnite()); + LocalDateTime dateFinPrecedente = + periodeAnalyse.getDateFin().minus(periodeAnalyse.getDuree(), periodeAnalyse.getUnite()); + + return switch (typeMetrique) { + case NOMBRE_MEMBRES_ACTIFS -> + calculerNombreMembresActifs(organisationId, dateDebutPrecedente, dateFinPrecedente); + case TOTAL_COTISATIONS_COLLECTEES -> + calculerTotalCotisationsCollectees( + organisationId, dateDebutPrecedente, dateFinPrecedente); + case NOMBRE_EVENEMENTS_ORGANISES -> + calculerNombreEvenementsOrganises(organisationId, dateDebutPrecedente, dateFinPrecedente); + case NOMBRE_DEMANDES_AIDE -> + calculerNombreDemandesAide(organisationId, dateDebutPrecedente, dateFinPrecedente); + default -> BigDecimal.ZERO; + }; + } + + private BigDecimal calculerPourcentageEvolution( + BigDecimal valeurActuelle, BigDecimal valeurPrecedente) { + if (valeurPrecedente == null || valeurPrecedente.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + + return valeurActuelle + .subtract(valeurPrecedente) + .divide(valeurPrecedente, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + } + + private String obtenirNomOrganisation(UUID organisationId) { + // Temporairement désactivé pour éviter les erreurs de compilation + return "Organisation " + + (organisationId != null ? organisationId.toString().substring(0, 8) : "inconnue"); + } + + private DashboardWidgetDTO creerWidgetKPI( + TypeMetrique typeMetrique, + PeriodeAnalyse periodeAnalyse, + UUID organisationId, + UUID utilisateurId, + int positionX, + int positionY, + int largeur, + int hauteur) { + AnalyticsDataDTO data = calculerMetrique(typeMetrique, periodeAnalyse, organisationId); + + return DashboardWidgetDTO.builder() + .titre(typeMetrique.getLibelle()) + .typeWidget("kpi") + .typeMetrique(typeMetrique) + .periodeAnalyse(periodeAnalyse) + .organisationId(organisationId) + .utilisateurProprietaireId(utilisateurId) + .positionX(positionX) + .positionY(positionY) + .largeur(largeur) + .hauteur(hauteur) + .couleurPrincipale(typeMetrique.getCouleur()) + .icone(typeMetrique.getIcone()) + .donneesWidget(convertirEnJSON(data)) + .dateDerniereMiseAJour(LocalDateTime.now()) + .build(); + } + + private DashboardWidgetDTO creerWidgetGraphique( + TypeMetrique typeMetrique, + PeriodeAnalyse periodeAnalyse, + UUID organisationId, + UUID utilisateurId, + int positionX, + int positionY, + int largeur, + int hauteur, + String typeGraphique) { + KPITrendDTO trend = calculerTendanceKPI(typeMetrique, periodeAnalyse, organisationId); + + return DashboardWidgetDTO.builder() + .titre("Évolution " + typeMetrique.getLibelle()) + .typeWidget("chart") + .typeMetrique(typeMetrique) + .periodeAnalyse(periodeAnalyse) + .organisationId(organisationId) + .utilisateurProprietaireId(utilisateurId) + .positionX(positionX) + .positionY(positionY) + .largeur(largeur) + .hauteur(hauteur) + .couleurPrincipale(typeMetrique.getCouleur()) + .icone(typeMetrique.getIcone()) + .donneesWidget(convertirEnJSON(trend)) + .configurationVisuelle("{\"type\":\"" + typeGraphique + "\",\"responsive\":true}") + .dateDerniereMiseAJour(LocalDateTime.now()) + .build(); + } + + private String convertirEnJSON(Object data) { + // Implémentation simplifiée - utiliser Jackson en production + return "{}"; // À implémenter avec ObjectMapper + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/AuditService.java b/src/main/java/dev/lions/unionflow/server/service/AuditService.java new file mode 100644 index 0000000..a2fb126 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/AuditService.java @@ -0,0 +1,229 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.admin.AuditLogDTO; +import dev.lions.unionflow.server.entity.AuditLog; +import dev.lions.unionflow.server.repository.AuditLogRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +/** + * Service pour la gestion des logs d'audit + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-17 + */ +@ApplicationScoped +@Slf4j +public class AuditService { + + @Inject + AuditLogRepository auditLogRepository; + + /** + * Enregistre un nouveau log d'audit + */ + @Transactional + public AuditLogDTO enregistrerLog(AuditLogDTO dto) { + log.debug("Enregistrement d'un log d'audit: {}", dto.getTypeAction()); + + AuditLog auditLog = convertToEntity(dto); + auditLogRepository.persist(auditLog); + + return convertToDTO(auditLog); + } + + /** + * Récupère tous les logs avec pagination + */ + public Map listerTous(int page, int size, String sortBy, String sortOrder) { + log.debug("Récupération des logs d'audit - page: {}, size: {}", page, size); + + String orderBy = sortBy != null ? sortBy : "dateHeure"; + String order = "desc".equalsIgnoreCase(sortOrder) ? "DESC" : "ASC"; + + var entityManager = auditLogRepository.getEntityManager(); + + // Compter le total + long total = auditLogRepository.count(); + + // Récupérer les logs avec pagination + var query = entityManager.createQuery( + "SELECT a FROM AuditLog a ORDER BY a." + orderBy + " " + order, AuditLog.class); + query.setFirstResult(page * size); + query.setMaxResults(size); + + List logs = query.getResultList(); + List dtos = logs.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return Map.of( + "data", dtos, + "total", total, + "page", page, + "size", size, + "totalPages", (int) Math.ceil((double) total / size) + ); + } + + /** + * Recherche les logs avec filtres + */ + public Map rechercher( + LocalDateTime dateDebut, LocalDateTime dateFin, + String typeAction, String severite, String utilisateur, + String module, String ipAddress, + int page, int size) { + + log.debug("Recherche de logs d'audit avec filtres"); + + // Construire la requête dynamique avec Criteria API + var entityManager = auditLogRepository.getEntityManager(); + var cb = entityManager.getCriteriaBuilder(); + var query = cb.createQuery(AuditLog.class); + var root = query.from(AuditLog.class); + + var predicates = new ArrayList(); + + if (dateDebut != null) { + predicates.add(cb.greaterThanOrEqualTo(root.get("dateHeure"), dateDebut)); + } + if (dateFin != null) { + predicates.add(cb.lessThanOrEqualTo(root.get("dateHeure"), dateFin)); + } + if (typeAction != null && !typeAction.isEmpty()) { + predicates.add(cb.equal(root.get("typeAction"), typeAction)); + } + if (severite != null && !severite.isEmpty()) { + predicates.add(cb.equal(root.get("severite"), severite)); + } + if (utilisateur != null && !utilisateur.isEmpty()) { + predicates.add(cb.like(cb.lower(root.get("utilisateur")), + "%" + utilisateur.toLowerCase() + "%")); + } + if (module != null && !module.isEmpty()) { + predicates.add(cb.equal(root.get("module"), module)); + } + if (ipAddress != null && !ipAddress.isEmpty()) { + predicates.add(cb.like(root.get("ipAddress"), "%" + ipAddress + "%")); + } + + query.where(predicates.toArray(new jakarta.persistence.criteria.Predicate[0])); + query.orderBy(cb.desc(root.get("dateHeure"))); + + // Compter le total + var countQuery = cb.createQuery(Long.class); + countQuery.select(cb.count(countQuery.from(AuditLog.class))); + countQuery.where(predicates.toArray(new jakarta.persistence.criteria.Predicate[0])); + long total = entityManager.createQuery(countQuery).getSingleResult(); + + // Récupérer les résultats avec pagination + var typedQuery = entityManager.createQuery(query); + typedQuery.setFirstResult(page * size); + typedQuery.setMaxResults(size); + + List logs = typedQuery.getResultList(); + List dtos = logs.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + + return Map.of( + "data", dtos, + "total", total, + "page", page, + "size", size, + "totalPages", (int) Math.ceil((double) total / size) + ); + } + + /** + * Récupère les statistiques d'audit + */ + public Map getStatistiques() { + long total = auditLogRepository.count(); + + var entityManager = auditLogRepository.getEntityManager(); + + long success = entityManager.createQuery( + "SELECT COUNT(a) FROM AuditLog a WHERE a.severite = :severite", Long.class) + .setParameter("severite", "SUCCESS") + .getSingleResult(); + + long errors = entityManager.createQuery( + "SELECT COUNT(a) FROM AuditLog a WHERE a.severite IN :severites", Long.class) + .setParameter("severites", List.of("ERROR", "CRITICAL")) + .getSingleResult(); + + long warnings = entityManager.createQuery( + "SELECT COUNT(a) FROM AuditLog a WHERE a.severite = :severite", Long.class) + .setParameter("severite", "WARNING") + .getSingleResult(); + + return Map.of( + "total", total, + "success", success, + "errors", errors, + "warnings", warnings + ); + } + + /** + * Convertit une entité en DTO + */ + private AuditLogDTO convertToDTO(AuditLog auditLog) { + AuditLogDTO dto = new AuditLogDTO(); + dto.setId(auditLog.getId()); + dto.setTypeAction(auditLog.getTypeAction()); + dto.setSeverite(auditLog.getSeverite()); + dto.setUtilisateur(auditLog.getUtilisateur()); + dto.setRole(auditLog.getRole()); + dto.setModule(auditLog.getModule()); + dto.setDescription(auditLog.getDescription()); + dto.setDetails(auditLog.getDetails()); + dto.setIpAddress(auditLog.getIpAddress()); + dto.setUserAgent(auditLog.getUserAgent()); + dto.setSessionId(auditLog.getSessionId()); + dto.setDateHeure(auditLog.getDateHeure()); + dto.setDonneesAvant(auditLog.getDonneesAvant()); + dto.setDonneesApres(auditLog.getDonneesApres()); + dto.setEntiteId(auditLog.getEntiteId()); + dto.setEntiteType(auditLog.getEntiteType()); + return dto; + } + + /** + * Convertit un DTO en entité + */ + private AuditLog convertToEntity(AuditLogDTO dto) { + AuditLog auditLog = new AuditLog(); + if (dto.getId() != null) { + auditLog.setId(dto.getId()); + } + auditLog.setTypeAction(dto.getTypeAction()); + auditLog.setSeverite(dto.getSeverite()); + auditLog.setUtilisateur(dto.getUtilisateur()); + auditLog.setRole(dto.getRole()); + auditLog.setModule(dto.getModule()); + auditLog.setDescription(dto.getDescription()); + auditLog.setDetails(dto.getDetails()); + auditLog.setIpAddress(dto.getIpAddress()); + auditLog.setUserAgent(dto.getUserAgent()); + auditLog.setSessionId(dto.getSessionId()); + auditLog.setDateHeure(dto.getDateHeure() != null ? dto.getDateHeure() : LocalDateTime.now()); + auditLog.setDonneesAvant(dto.getDonneesAvant()); + auditLog.setDonneesApres(dto.getDonneesApres()); + auditLog.setEntiteId(dto.getEntiteId()); + auditLog.setEntiteType(dto.getEntiteType()); + return auditLog; + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/service/ComptabiliteService.java b/src/main/java/dev/lions/unionflow/server/service/ComptabiliteService.java new file mode 100644 index 0000000..8a68cc0 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/ComptabiliteService.java @@ -0,0 +1,479 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.comptabilite.*; +import dev.lions.unionflow.server.entity.*; +import dev.lions.unionflow.server.repository.*; +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** + * Service métier pour la gestion comptable + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class ComptabiliteService { + + private static final Logger LOG = Logger.getLogger(ComptabiliteService.class); + + @Inject CompteComptableRepository compteComptableRepository; + + @Inject JournalComptableRepository journalComptableRepository; + + @Inject EcritureComptableRepository ecritureComptableRepository; + + @Inject LigneEcritureRepository ligneEcritureRepository; + + @Inject OrganisationRepository organisationRepository; + + @Inject PaiementRepository paiementRepository; + + @Inject KeycloakService keycloakService; + + // ======================================== + // COMPTES COMPTABLES + // ======================================== + + /** + * Crée un nouveau compte comptable + * + * @param compteDTO DTO du compte à créer + * @return DTO du compte créé + */ + @Transactional + public CompteComptableDTO creerCompteComptable(CompteComptableDTO compteDTO) { + LOG.infof("Création d'un nouveau compte comptable: %s", compteDTO.getNumeroCompte()); + + // Vérifier l'unicité du numéro + if (compteComptableRepository.findByNumeroCompte(compteDTO.getNumeroCompte()).isPresent()) { + throw new IllegalArgumentException("Un compte avec ce numéro existe déjà: " + compteDTO.getNumeroCompte()); + } + + CompteComptable compte = convertToEntity(compteDTO); + compte.setCreePar(keycloakService.getCurrentUserEmail()); + + compteComptableRepository.persist(compte); + LOG.infof("Compte comptable créé avec succès: ID=%s, Numéro=%s", compte.getId(), compte.getNumeroCompte()); + + return convertToDTO(compte); + } + + /** + * Trouve un compte comptable par son ID + * + * @param id ID du compte + * @return DTO du compte + */ + public CompteComptableDTO trouverCompteParId(UUID id) { + return compteComptableRepository + .findCompteComptableById(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Compte comptable non trouvé avec l'ID: " + id)); + } + + /** + * Liste tous les comptes comptables actifs + * + * @return Liste des comptes + */ + public List listerTousLesComptes() { + return compteComptableRepository.findAllActifs().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + // ======================================== + // JOURNAUX COMPTABLES + // ======================================== + + /** + * Crée un nouveau journal comptable + * + * @param journalDTO DTO du journal à créer + * @return DTO du journal créé + */ + @Transactional + public JournalComptableDTO creerJournalComptable(JournalComptableDTO journalDTO) { + LOG.infof("Création d'un nouveau journal comptable: %s", journalDTO.getCode()); + + // Vérifier l'unicité du code + if (journalComptableRepository.findByCode(journalDTO.getCode()).isPresent()) { + throw new IllegalArgumentException("Un journal avec ce code existe déjà: " + journalDTO.getCode()); + } + + JournalComptable journal = convertToEntity(journalDTO); + journal.setCreePar(keycloakService.getCurrentUserEmail()); + + journalComptableRepository.persist(journal); + LOG.infof("Journal comptable créé avec succès: ID=%s, Code=%s", journal.getId(), journal.getCode()); + + return convertToDTO(journal); + } + + /** + * Trouve un journal comptable par son ID + * + * @param id ID du journal + * @return DTO du journal + */ + public JournalComptableDTO trouverJournalParId(UUID id) { + return journalComptableRepository + .findJournalComptableById(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Journal comptable non trouvé avec l'ID: " + id)); + } + + /** + * Liste tous les journaux comptables actifs + * + * @return Liste des journaux + */ + public List listerTousLesJournaux() { + return journalComptableRepository.findAllActifs().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + // ======================================== + // ÉCRITURES COMPTABLES + // ======================================== + + /** + * Crée une nouvelle écriture comptable avec validation de l'équilibre + * + * @param ecritureDTO DTO de l'écriture à créer + * @return DTO de l'écriture créée + */ + @Transactional + public EcritureComptableDTO creerEcritureComptable(EcritureComptableDTO ecritureDTO) { + LOG.infof("Création d'une nouvelle écriture comptable: %s", ecritureDTO.getNumeroPiece()); + + // Vérifier l'équilibre + if (!isEcritureEquilibree(ecritureDTO)) { + throw new IllegalArgumentException("L'écriture n'est pas équilibrée (Débit ≠ Crédit)"); + } + + EcritureComptable ecriture = convertToEntity(ecritureDTO); + ecriture.setCreePar(keycloakService.getCurrentUserEmail()); + + // Calculer les totaux + ecriture.calculerTotaux(); + + ecritureComptableRepository.persist(ecriture); + LOG.infof("Écriture comptable créée avec succès: ID=%s, Numéro=%s", ecriture.getId(), ecriture.getNumeroPiece()); + + return convertToDTO(ecriture); + } + + /** + * Trouve une écriture comptable par son ID + * + * @param id ID de l'écriture + * @return DTO de l'écriture + */ + public EcritureComptableDTO trouverEcritureParId(UUID id) { + return ecritureComptableRepository + .findEcritureComptableById(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Écriture comptable non trouvée avec l'ID: " + id)); + } + + /** + * Liste les écritures d'un journal + * + * @param journalId ID du journal + * @return Liste des écritures + */ + public List listerEcrituresParJournal(UUID journalId) { + return ecritureComptableRepository.findByJournalId(journalId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Liste les écritures d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des écritures + */ + public List listerEcrituresParOrganisation(UUID organisationId) { + return ecritureComptableRepository.findByOrganisationId(organisationId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + // ======================================== + // MÉTHODES PRIVÉES - CONVERSIONS + // ======================================== + + /** Vérifie si une écriture est équilibrée */ + private boolean isEcritureEquilibree(EcritureComptableDTO ecritureDTO) { + if (ecritureDTO.getLignes() == null || ecritureDTO.getLignes().isEmpty()) { + return false; + } + + BigDecimal totalDebit = + ecritureDTO.getLignes().stream() + .map(LigneEcritureDTO::getMontantDebit) + .filter(amount -> amount != null) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal totalCredit = + ecritureDTO.getLignes().stream() + .map(LigneEcritureDTO::getMontantCredit) + .filter(amount -> amount != null) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + return totalDebit.compareTo(totalCredit) == 0; + } + + /** Convertit une entité CompteComptable en DTO */ + private CompteComptableDTO convertToDTO(CompteComptable compte) { + if (compte == null) { + return null; + } + + CompteComptableDTO dto = new CompteComptableDTO(); + dto.setId(compte.getId()); + dto.setNumeroCompte(compte.getNumeroCompte()); + dto.setLibelle(compte.getLibelle()); + dto.setTypeCompte(compte.getTypeCompte()); + dto.setClasseComptable(compte.getClasseComptable()); + dto.setSoldeInitial(compte.getSoldeInitial()); + dto.setSoldeActuel(compte.getSoldeActuel()); + dto.setCompteCollectif(compte.getCompteCollectif()); + dto.setCompteAnalytique(compte.getCompteAnalytique()); + dto.setDescription(compte.getDescription()); + dto.setDateCreation(compte.getDateCreation()); + dto.setDateModification(compte.getDateModification()); + dto.setActif(compte.getActif()); + + return dto; + } + + /** Convertit un DTO en entité CompteComptable */ + private CompteComptable convertToEntity(CompteComptableDTO dto) { + if (dto == null) { + return null; + } + + CompteComptable compte = new CompteComptable(); + compte.setNumeroCompte(dto.getNumeroCompte()); + compte.setLibelle(dto.getLibelle()); + compte.setTypeCompte(dto.getTypeCompte()); + compte.setClasseComptable(dto.getClasseComptable()); + compte.setSoldeInitial(dto.getSoldeInitial() != null ? dto.getSoldeInitial() : BigDecimal.ZERO); + compte.setSoldeActuel(dto.getSoldeActuel() != null ? dto.getSoldeActuel() : dto.getSoldeInitial()); + compte.setCompteCollectif(dto.getCompteCollectif() != null ? dto.getCompteCollectif() : false); + compte.setCompteAnalytique(dto.getCompteAnalytique() != null ? dto.getCompteAnalytique() : false); + compte.setDescription(dto.getDescription()); + + return compte; + } + + /** Convertit une entité JournalComptable en DTO */ + private JournalComptableDTO convertToDTO(JournalComptable journal) { + if (journal == null) { + return null; + } + + JournalComptableDTO dto = new JournalComptableDTO(); + dto.setId(journal.getId()); + dto.setCode(journal.getCode()); + dto.setLibelle(journal.getLibelle()); + dto.setTypeJournal(journal.getTypeJournal()); + dto.setDateDebut(journal.getDateDebut()); + dto.setDateFin(journal.getDateFin()); + dto.setStatut(journal.getStatut()); + dto.setDescription(journal.getDescription()); + dto.setDateCreation(journal.getDateCreation()); + dto.setDateModification(journal.getDateModification()); + dto.setActif(journal.getActif()); + + return dto; + } + + /** Convertit un DTO en entité JournalComptable */ + private JournalComptable convertToEntity(JournalComptableDTO dto) { + if (dto == null) { + return null; + } + + JournalComptable journal = new JournalComptable(); + journal.setCode(dto.getCode()); + journal.setLibelle(dto.getLibelle()); + journal.setTypeJournal(dto.getTypeJournal()); + journal.setDateDebut(dto.getDateDebut()); + journal.setDateFin(dto.getDateFin()); + journal.setStatut(dto.getStatut() != null ? dto.getStatut() : "OUVERT"); + journal.setDescription(dto.getDescription()); + + return journal; + } + + /** Convertit une entité EcritureComptable en DTO */ + private EcritureComptableDTO convertToDTO(EcritureComptable ecriture) { + if (ecriture == null) { + return null; + } + + EcritureComptableDTO dto = new EcritureComptableDTO(); + dto.setId(ecriture.getId()); + dto.setNumeroPiece(ecriture.getNumeroPiece()); + dto.setDateEcriture(ecriture.getDateEcriture()); + dto.setLibelle(ecriture.getLibelle()); + dto.setReference(ecriture.getReference()); + dto.setLettrage(ecriture.getLettrage()); + dto.setPointe(ecriture.getPointe()); + dto.setMontantDebit(ecriture.getMontantDebit()); + dto.setMontantCredit(ecriture.getMontantCredit()); + dto.setCommentaire(ecriture.getCommentaire()); + + if (ecriture.getJournal() != null) { + dto.setJournalId(ecriture.getJournal().getId()); + } + if (ecriture.getOrganisation() != null) { + dto.setOrganisationId(ecriture.getOrganisation().getId()); + } + if (ecriture.getPaiement() != null) { + dto.setPaiementId(ecriture.getPaiement().getId()); + } + + // Convertir les lignes + if (ecriture.getLignes() != null) { + dto.setLignes( + ecriture.getLignes().stream().map(this::convertToDTO).collect(Collectors.toList())); + } + + dto.setDateCreation(ecriture.getDateCreation()); + dto.setDateModification(ecriture.getDateModification()); + dto.setActif(ecriture.getActif()); + + return dto; + } + + /** Convertit un DTO en entité EcritureComptable */ + private EcritureComptable convertToEntity(EcritureComptableDTO dto) { + if (dto == null) { + return null; + } + + EcritureComptable ecriture = new EcritureComptable(); + ecriture.setNumeroPiece(dto.getNumeroPiece()); + ecriture.setDateEcriture(dto.getDateEcriture() != null ? dto.getDateEcriture() : LocalDate.now()); + ecriture.setLibelle(dto.getLibelle()); + ecriture.setReference(dto.getReference()); + ecriture.setLettrage(dto.getLettrage()); + ecriture.setPointe(dto.getPointe() != null ? dto.getPointe() : false); + ecriture.setCommentaire(dto.getCommentaire()); + + // Relations + if (dto.getJournalId() != null) { + JournalComptable journal = + journalComptableRepository + .findJournalComptableById(dto.getJournalId()) + .orElseThrow( + () -> new NotFoundException("Journal comptable non trouvé avec l'ID: " + dto.getJournalId())); + ecriture.setJournal(journal); + } + + if (dto.getOrganisationId() != null) { + Organisation org = + organisationRepository + .findByIdOptional(dto.getOrganisationId()) + .orElseThrow( + () -> + new NotFoundException( + "Organisation non trouvée avec l'ID: " + dto.getOrganisationId())); + ecriture.setOrganisation(org); + } + + if (dto.getPaiementId() != null) { + Paiement paiement = + paiementRepository + .findPaiementById(dto.getPaiementId()) + .orElseThrow( + () -> new NotFoundException("Paiement non trouvé avec l'ID: " + dto.getPaiementId())); + ecriture.setPaiement(paiement); + } + + // Convertir les lignes + if (dto.getLignes() != null) { + for (LigneEcritureDTO ligneDTO : dto.getLignes()) { + LigneEcriture ligne = convertToEntity(ligneDTO); + ligne.setEcriture(ecriture); + ecriture.getLignes().add(ligne); + } + } + + return ecriture; + } + + /** Convertit une entité LigneEcriture en DTO */ + private LigneEcritureDTO convertToDTO(LigneEcriture ligne) { + if (ligne == null) { + return null; + } + + LigneEcritureDTO dto = new LigneEcritureDTO(); + dto.setId(ligne.getId()); + dto.setNumeroLigne(ligne.getNumeroLigne()); + dto.setMontantDebit(ligne.getMontantDebit()); + dto.setMontantCredit(ligne.getMontantCredit()); + dto.setLibelle(ligne.getLibelle()); + dto.setReference(ligne.getReference()); + + if (ligne.getEcriture() != null) { + dto.setEcritureId(ligne.getEcriture().getId()); + } + if (ligne.getCompteComptable() != null) { + dto.setCompteComptableId(ligne.getCompteComptable().getId()); + } + + dto.setDateCreation(ligne.getDateCreation()); + dto.setDateModification(ligne.getDateModification()); + dto.setActif(ligne.getActif()); + + return dto; + } + + /** Convertit un DTO en entité LigneEcriture */ + private LigneEcriture convertToEntity(LigneEcritureDTO dto) { + if (dto == null) { + return null; + } + + LigneEcriture ligne = new LigneEcriture(); + ligne.setNumeroLigne(dto.getNumeroLigne()); + ligne.setMontantDebit(dto.getMontantDebit() != null ? dto.getMontantDebit() : BigDecimal.ZERO); + ligne.setMontantCredit(dto.getMontantCredit() != null ? dto.getMontantCredit() : BigDecimal.ZERO); + ligne.setLibelle(dto.getLibelle()); + ligne.setReference(dto.getReference()); + + // Relation CompteComptable + if (dto.getCompteComptableId() != null) { + CompteComptable compte = + compteComptableRepository + .findCompteComptableById(dto.getCompteComptableId()) + .orElseThrow( + () -> + new NotFoundException( + "Compte comptable non trouvé avec l'ID: " + dto.getCompteComptableId())); + ligne.setCompteComptable(compte); + } + + return ligne; + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/service/CotisationService.java b/src/main/java/dev/lions/unionflow/server/service/CotisationService.java new file mode 100644 index 0000000..a475c28 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/CotisationService.java @@ -0,0 +1,493 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.finance.CotisationDTO; +import dev.lions.unionflow.server.entity.Cotisation; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.repository.CotisationRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +/** + * Service métier pour la gestion des cotisations Contient la logique métier et les règles de + * validation + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@ApplicationScoped +@Slf4j +public class CotisationService { + + @Inject CotisationRepository cotisationRepository; + + @Inject MembreRepository membreRepository; + + /** + * Récupère toutes les cotisations avec pagination + * + * @param page numéro de page (0-based) + * @param size taille de la page + * @return liste des cotisations converties en DTO + */ + public List getAllCotisations(int page, int size) { + log.debug("Récupération des cotisations - page: {}, size: {}", page, size); + + // Utilisation de EntityManager pour la pagination + jakarta.persistence.TypedQuery query = + cotisationRepository.getEntityManager().createQuery( + "SELECT c FROM Cotisation c ORDER BY c.dateEcheance DESC", + Cotisation.class); + query.setFirstResult(page * size); + query.setMaxResults(size); + List cotisations = query.getResultList(); + + return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère une cotisation par son ID + * + * @param id identifiant UUID de la cotisation + * @return DTO de la cotisation + * @throws NotFoundException si la cotisation n'existe pas + */ + public CotisationDTO getCotisationById(@NotNull UUID id) { + log.debug("Récupération de la cotisation avec ID: {}", id); + + Cotisation cotisation = + cotisationRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id)); + + return convertToDTO(cotisation); + } + + /** + * Récupère une cotisation par son numéro de référence + * + * @param numeroReference numéro de référence unique + * @return DTO de la cotisation + * @throws NotFoundException si la cotisation n'existe pas + */ + public CotisationDTO getCotisationByReference(@NotNull String numeroReference) { + log.debug("Récupération de la cotisation avec référence: {}", numeroReference); + + Cotisation cotisation = + cotisationRepository + .findByNumeroReference(numeroReference) + .orElseThrow( + () -> + new NotFoundException( + "Cotisation non trouvée avec la référence: " + numeroReference)); + + return convertToDTO(cotisation); + } + + /** + * Crée une nouvelle cotisation + * + * @param cotisationDTO données de la cotisation à créer + * @return DTO de la cotisation créée + */ + @Transactional + public CotisationDTO createCotisation(@Valid CotisationDTO cotisationDTO) { + log.info("Création d'une nouvelle cotisation pour le membre: {}", cotisationDTO.getMembreId()); + + // Validation du membre - UUID direct maintenant + Membre membre = + membreRepository + .findByIdOptional(cotisationDTO.getMembreId()) + .orElseThrow( + () -> + new NotFoundException( + "Membre non trouvé avec l'ID: " + cotisationDTO.getMembreId())); + + // Conversion DTO vers entité + Cotisation cotisation = convertToEntity(cotisationDTO); + cotisation.setMembre(membre); + + // Génération automatique du numéro de référence si absent + if (cotisation.getNumeroReference() == null || cotisation.getNumeroReference().isEmpty()) { + cotisation.setNumeroReference(Cotisation.genererNumeroReference()); + } + + // Validation des règles métier + validateCotisationRules(cotisation); + + // Persistance + cotisationRepository.persist(cotisation); + + log.info( + "Cotisation créée avec succès - ID: {}, Référence: {}", + cotisation.getId(), + cotisation.getNumeroReference()); + + return convertToDTO(cotisation); + } + + /** + * Met à jour une cotisation existante + * + * @param id identifiant UUID de la cotisation + * @param cotisationDTO nouvelles données + * @return DTO de la cotisation mise à jour + */ + @Transactional + public CotisationDTO updateCotisation(@NotNull UUID id, @Valid CotisationDTO cotisationDTO) { + log.info("Mise à jour de la cotisation avec ID: {}", id); + + Cotisation cotisationExistante = + cotisationRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id)); + + // Mise à jour des champs modifiables + updateCotisationFields(cotisationExistante, cotisationDTO); + + // Validation des règles métier + validateCotisationRules(cotisationExistante); + + log.info("Cotisation mise à jour avec succès - ID: {}", id); + + return convertToDTO(cotisationExistante); + } + + /** + * Supprime (désactive) une cotisation + * + * @param id identifiant UUID de la cotisation + */ + @Transactional + public void deleteCotisation(@NotNull UUID id) { + log.info("Suppression de la cotisation avec ID: {}", id); + + Cotisation cotisation = + cotisationRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Cotisation non trouvée avec l'ID: " + id)); + + // Vérification si la cotisation peut être supprimée + if ("PAYEE".equals(cotisation.getStatut())) { + throw new IllegalStateException("Impossible de supprimer une cotisation déjà payée"); + } + + cotisation.setStatut("ANNULEE"); + + log.info("Cotisation supprimée avec succès - ID: {}", id); + } + + /** + * Récupère les cotisations d'un membre + * + * @param membreId identifiant UUID du membre + * @param page numéro de page + * @param size taille de la page + * @return liste des cotisations du membre + */ + public List getCotisationsByMembre(@NotNull UUID membreId, int page, int size) { + log.debug("Récupération des cotisations du membre: {}", membreId); + + // Vérification de l'existence du membre + if (!membreRepository.findByIdOptional(membreId).isPresent()) { + throw new NotFoundException("Membre non trouvé avec l'ID: " + membreId); + } + + List cotisations = + cotisationRepository.findByMembreId( + membreId, Page.of(page, size), Sort.by("dateEcheance").descending()); + + return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les cotisations par statut + * + * @param statut statut recherché + * @param page numéro de page + * @param size taille de la page + * @return liste des cotisations avec le statut spécifié + */ + public List getCotisationsByStatut(@NotNull String statut, int page, int size) { + log.debug("Récupération des cotisations avec statut: {}", statut); + + List cotisations = cotisationRepository.findByStatut(statut, Page.of(page, size)); + + return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les cotisations en retard + * + * @param page numéro de page + * @param size taille de la page + * @return liste des cotisations en retard + */ + public List getCotisationsEnRetard(int page, int size) { + log.debug("Récupération des cotisations en retard"); + + List cotisations = + cotisationRepository.findCotisationsEnRetard(LocalDate.now(), Page.of(page, size)); + + return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Recherche avancée de cotisations + * + * @param membreId identifiant du membre (optionnel) + * @param statut statut (optionnel) + * @param typeCotisation type (optionnel) + * @param annee année (optionnel) + * @param mois mois (optionnel) + * @param page numéro de page + * @param size taille de la page + * @return liste filtrée des cotisations + */ + public List rechercherCotisations( + UUID membreId, + String statut, + String typeCotisation, + Integer annee, + Integer mois, + int page, + int size) { + log.debug("Recherche avancée de cotisations avec filtres"); + + List cotisations = + cotisationRepository.rechercheAvancee( + membreId, statut, typeCotisation, annee, mois, Page.of(page, size)); + + return cotisations.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** + * Récupère les statistiques des cotisations + * + * @return map contenant les statistiques + */ + public Map getStatistiquesCotisations() { + log.debug("Calcul des statistiques des cotisations"); + + long totalCotisations = cotisationRepository.count(); + long cotisationsPayees = cotisationRepository.compterParStatut("PAYEE"); + long cotisationsEnRetard = + cotisationRepository + .findCotisationsEnRetard(LocalDate.now(), Page.of(0, Integer.MAX_VALUE)) + .size(); + + return Map.of( + "totalCotisations", totalCotisations, + "cotisationsPayees", cotisationsPayees, + "cotisationsEnRetard", cotisationsEnRetard, + "tauxPaiement", + totalCotisations > 0 ? (cotisationsPayees * 100.0 / totalCotisations) : 0.0); + } + + /** Convertit une entité Cotisation en DTO */ + private CotisationDTO convertToDTO(Cotisation cotisation) { + if (cotisation == null) { + return null; + } + + CotisationDTO dto = new CotisationDTO(); + + // Conversion de l'ID UUID vers UUID (pas de conversion nécessaire maintenant) + dto.setId(cotisation.getId()); + dto.setNumeroReference(cotisation.getNumeroReference()); + + // Conversion du membre associé + if (cotisation.getMembre() != null) { + dto.setMembreId(cotisation.getMembre().getId()); + dto.setNomMembre(cotisation.getMembre().getNomComplet()); + dto.setNumeroMembre(cotisation.getMembre().getNumeroMembre()); + + // Conversion de l'organisation du membre (associationId) + if (cotisation.getMembre().getOrganisation() != null + && cotisation.getMembre().getOrganisation().getId() != null) { + dto.setAssociationId(cotisation.getMembre().getOrganisation().getId()); + dto.setNomAssociation(cotisation.getMembre().getOrganisation().getNom()); + } + } + + // Propriétés de la cotisation + dto.setTypeCotisation(cotisation.getTypeCotisation()); + dto.setMontantDu(cotisation.getMontantDu()); + dto.setMontantPaye(cotisation.getMontantPaye()); + dto.setCodeDevise(cotisation.getCodeDevise()); + dto.setStatut(cotisation.getStatut()); + dto.setDateEcheance(cotisation.getDateEcheance()); + dto.setDatePaiement(cotisation.getDatePaiement()); + dto.setDescription(cotisation.getDescription()); + dto.setPeriode(cotisation.getPeriode()); + dto.setAnnee(cotisation.getAnnee()); + dto.setMois(cotisation.getMois()); + dto.setObservations(cotisation.getObservations()); + dto.setRecurrente(cotisation.getRecurrente()); + dto.setNombreRappels(cotisation.getNombreRappels()); + dto.setDateDernierRappel(cotisation.getDateDernierRappel()); + + // Conversion du validateur + dto.setValidePar( + cotisation.getValideParId() != null + ? cotisation.getValideParId() + : null); + dto.setNomValidateur(cotisation.getNomValidateur()); + + dto.setMethodePaiement(cotisation.getMethodePaiement()); + dto.setReferencePaiement(cotisation.getReferencePaiement()); + dto.setDateCreation(cotisation.getDateCreation()); + dto.setDateModification(cotisation.getDateModification()); + + // Propriétés héritées de BaseDTO + dto.setActif(true); // Les cotisations sont toujours actives + dto.setVersion(0L); // Version par défaut + + return dto; + } + + /** Convertit un DTO en entité Cotisation */ + private Cotisation convertToEntity(CotisationDTO dto) { + return Cotisation.builder() + .numeroReference(dto.getNumeroReference()) + .typeCotisation(dto.getTypeCotisation()) + .montantDu(dto.getMontantDu()) + .montantPaye(dto.getMontantPaye() != null ? dto.getMontantPaye() : BigDecimal.ZERO) + .codeDevise(dto.getCodeDevise() != null ? dto.getCodeDevise() : "XOF") + .statut(dto.getStatut() != null ? dto.getStatut() : "EN_ATTENTE") + .dateEcheance(dto.getDateEcheance()) + .datePaiement(dto.getDatePaiement()) + .description(dto.getDescription()) + .periode(dto.getPeriode()) + .annee(dto.getAnnee()) + .mois(dto.getMois()) + .observations(dto.getObservations()) + .recurrente(dto.getRecurrente() != null ? dto.getRecurrente() : false) + .nombreRappels(dto.getNombreRappels() != null ? dto.getNombreRappels() : 0) + .dateDernierRappel(dto.getDateDernierRappel()) + .methodePaiement(dto.getMethodePaiement()) + .referencePaiement(dto.getReferencePaiement()) + .build(); + } + + /** Met à jour les champs d'une cotisation existante */ + private void updateCotisationFields(Cotisation cotisation, CotisationDTO dto) { + if (dto.getTypeCotisation() != null) { + cotisation.setTypeCotisation(dto.getTypeCotisation()); + } + if (dto.getMontantDu() != null) { + cotisation.setMontantDu(dto.getMontantDu()); + } + if (dto.getMontantPaye() != null) { + cotisation.setMontantPaye(dto.getMontantPaye()); + } + if (dto.getStatut() != null) { + cotisation.setStatut(dto.getStatut()); + } + if (dto.getDateEcheance() != null) { + cotisation.setDateEcheance(dto.getDateEcheance()); + } + if (dto.getDatePaiement() != null) { + cotisation.setDatePaiement(dto.getDatePaiement()); + } + if (dto.getDescription() != null) { + cotisation.setDescription(dto.getDescription()); + } + if (dto.getObservations() != null) { + cotisation.setObservations(dto.getObservations()); + } + if (dto.getMethodePaiement() != null) { + cotisation.setMethodePaiement(dto.getMethodePaiement()); + } + if (dto.getReferencePaiement() != null) { + cotisation.setReferencePaiement(dto.getReferencePaiement()); + } + } + + /** Valide les règles métier pour une cotisation */ + private void validateCotisationRules(Cotisation cotisation) { + // Validation du montant + if (cotisation.getMontantDu().compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Le montant dû doit être positif"); + } + + // Validation de la date d'échéance + if (cotisation.getDateEcheance().isBefore(LocalDate.now().minusYears(1))) { + throw new IllegalArgumentException("La date d'échéance ne peut pas être antérieure à un an"); + } + + // Validation du montant payé + if (cotisation.getMontantPaye().compareTo(cotisation.getMontantDu()) > 0) { + throw new IllegalArgumentException("Le montant payé ne peut pas dépasser le montant dû"); + } + + // Validation de la cohérence statut/paiement + if ("PAYEE".equals(cotisation.getStatut()) + && cotisation.getMontantPaye().compareTo(cotisation.getMontantDu()) < 0) { + throw new IllegalArgumentException( + "Une cotisation marquée comme payée doit avoir un montant payé égal au montant dû"); + } + } + + /** + * Envoie des rappels de cotisations groupés à plusieurs membres (WOU/DRY) + * + * @param membreIds Liste des IDs des membres destinataires + * @return Nombre de rappels envoyés + */ + @Transactional + public int envoyerRappelsCotisationsGroupes(List membreIds) { + log.info("Envoi de rappels de cotisations groupés à {} membres", membreIds.size()); + + if (membreIds == null || membreIds.isEmpty()) { + throw new IllegalArgumentException("La liste des membres ne peut pas être vide"); + } + + int rappelsEnvoyes = 0; + for (UUID membreId : membreIds) { + try { + Membre membre = + membreRepository + .findByIdOptional(membreId) + .orElseThrow( + () -> + new IllegalArgumentException( + "Membre non trouvé avec l'ID: " + membreId)); + + // Trouver les cotisations en retard pour ce membre + List cotisationsEnRetard = + cotisationRepository.findCotisationsAuRappel(7, 3).stream() + .filter(c -> c.getMembre() != null && c.getMembre().getId().equals(membreId)) + .collect(Collectors.toList()); + + for (Cotisation cotisation : cotisationsEnRetard) { + // Incrémenter le nombre de rappels + cotisationRepository.incrementerNombreRappels(cotisation.getId()); + rappelsEnvoyes++; + } + } catch (Exception e) { + log.warn( + "Erreur lors de l'envoi du rappel de cotisation pour le membre {}: {}", + membreId, + e.getMessage()); + } + } + + log.info("{} rappels envoyés sur {} membres demandés", rappelsEnvoyes, membreIds.size()); + return rappelsEnvoyes; + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/DashboardServiceImpl.java b/src/main/java/dev/lions/unionflow/server/service/DashboardServiceImpl.java new file mode 100644 index 0000000..dea41dd --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/DashboardServiceImpl.java @@ -0,0 +1,254 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.dashboard.DashboardDataDTO; +import dev.lions.unionflow.server.api.dto.dashboard.DashboardStatsDTO; +import dev.lions.unionflow.server.api.dto.dashboard.RecentActivityDTO; +import dev.lions.unionflow.server.api.dto.dashboard.UpcomingEventDTO; +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.api.service.dashboard.DashboardService; +import dev.lions.unionflow.server.entity.Cotisation; +import dev.lions.unionflow.server.entity.DemandeAide; +import dev.lions.unionflow.server.entity.Evenement; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.repository.CotisationRepository; +import dev.lions.unionflow.server.repository.DemandeAideRepository; +import dev.lions.unionflow.server.repository.EvenementRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.persistence.TypedQuery; +import org.jboss.logging.Logger; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Implémentation du service Dashboard pour Quarkus + * + *

Cette implémentation récupère les données réelles depuis la base de données + * via les repositories. + * + * @author UnionFlow Team + * @version 2.0 + * @since 2025-01-17 + */ +@ApplicationScoped +public class DashboardServiceImpl implements DashboardService { + + private static final Logger LOG = Logger.getLogger(DashboardServiceImpl.class); + + @Inject + MembreRepository membreRepository; + + @Inject + EvenementRepository evenementRepository; + + @Inject + CotisationRepository cotisationRepository; + + @Inject + DemandeAideRepository demandeAideRepository; + + @Inject + OrganisationRepository organisationRepository; + + @Override + public DashboardDataDTO getDashboardData(String organizationId, String userId) { + LOG.infof("Récupération des données dashboard pour org: %s et user: %s", organizationId, userId); + + UUID orgId = UUID.fromString(organizationId); + + return DashboardDataDTO.builder() + .stats(getDashboardStats(organizationId, userId)) + .recentActivities(getRecentActivities(organizationId, userId, 10)) + .upcomingEvents(getUpcomingEvents(organizationId, userId, 5)) + .userPreferences(getUserPreferences(userId)) + .organizationId(organizationId) + .userId(userId) + .build(); + } + + @Override + public DashboardStatsDTO getDashboardStats(String organizationId, String userId) { + LOG.infof("Récupération des stats dashboard pour org: %s et user: %s", organizationId, userId); + + UUID orgId = UUID.fromString(organizationId); + + // Compter les membres + long totalMembers = membreRepository.count(); + long activeMembers = membreRepository.countActifs(); + + // Compter les événements + long totalEvents = evenementRepository.count(); + long upcomingEvents = evenementRepository.findEvenementsAVenir().size(); + + // Compter les cotisations + long totalContributions = cotisationRepository.count(); + BigDecimal totalContributionAmount = calculateTotalContributionAmount(orgId); + + // Compter les demandes en attente + List pendingRequests = demandeAideRepository.findByStatut(StatutAide.EN_ATTENTE); + long pendingRequestsCount = pendingRequests.stream() + .filter(d -> d.getOrganisation() != null && d.getOrganisation().getId().equals(orgId)) + .count(); + + // Calculer la croissance mensuelle (membres ajoutés ce mois) + LocalDate debutMois = LocalDate.now().withDayOfMonth(1); + long nouveauxMembresMois = membreRepository.countNouveauxMembres(debutMois); + long totalMembresAvant = totalMembers - nouveauxMembresMois; + double monthlyGrowth = totalMembresAvant > 0 + ? (double) nouveauxMembresMois / totalMembresAvant * 100.0 + : 0.0; + + // Calculer le taux d'engagement (membres actifs / total) + double engagementRate = totalMembers > 0 + ? (double) activeMembers / totalMembers + : 0.0; + + return DashboardStatsDTO.builder() + .totalMembers((int) totalMembers) + .activeMembers((int) activeMembers) + .totalEvents((int) totalEvents) + .upcomingEvents((int) upcomingEvents) + .totalContributions((int) totalContributions) + .totalContributionAmount(totalContributionAmount.doubleValue()) + .pendingRequests((int) pendingRequestsCount) + .completedProjects(0) // À implémenter si nécessaire + .monthlyGrowth(monthlyGrowth) + .engagementRate(engagementRate) + .lastUpdated(LocalDateTime.now()) + .build(); + } + + @Override + public List getRecentActivities(String organizationId, String userId, int limit) { + LOG.infof("Récupération de %d activités récentes pour org: %s et user: %s", limit, organizationId, userId); + + UUID orgId = UUID.fromString(organizationId); + List activities = new ArrayList<>(); + + // Récupérer les membres récemment créés + List nouveauxMembres = membreRepository.rechercheAvancee( + null, true, null, null, Page.of(0, limit), Sort.by("dateCreation", Sort.Direction.Descending)); + + for (Membre membre : nouveauxMembres) { + if (membre.getOrganisation() != null && membre.getOrganisation().getId().equals(orgId)) { + activities.add(RecentActivityDTO.builder() + .id(membre.getId().toString()) + .type("member") + .title("Nouveau membre inscrit") + .description(membre.getNomComplet() + " a rejoint l'organisation") + .userName(membre.getNomComplet()) + .timestamp(membre.getDateCreation()) + .userAvatar(null) + .actionUrl("/members/" + membre.getId()) + .build()); + } + } + + // Récupérer les événements récemment créés + List tousEvenements = evenementRepository.listAll(); + List nouveauxEvenements = tousEvenements.stream() + .filter(e -> e.getOrganisation() != null && e.getOrganisation().getId().equals(orgId)) + .sorted(Comparator.comparing(Evenement::getDateCreation).reversed()) + .limit(limit) + .collect(Collectors.toList()); + + for (Evenement evenement : nouveauxEvenements) { + activities.add(RecentActivityDTO.builder() + .id(evenement.getId().toString()) + .type("event") + .title("Événement créé") + .description(evenement.getTitre() + " a été programmé") + .userName(evenement.getOrganisation() != null ? evenement.getOrganisation().getNom() : "Système") + .timestamp(evenement.getDateCreation()) + .userAvatar(null) + .actionUrl("/events/" + evenement.getId()) + .build()); + } + + // Récupérer les cotisations récentes + List cotisationsRecentes = cotisationRepository.rechercheAvancee( + null, "PAYEE", null, null, null, Page.of(0, limit)); + + for (Cotisation cotisation : cotisationsRecentes) { + if (cotisation.getMembre() != null && + cotisation.getMembre().getOrganisation() != null && + cotisation.getMembre().getOrganisation().getId().equals(orgId)) { + activities.add(RecentActivityDTO.builder() + .id(cotisation.getId().toString()) + .type("contribution") + .title("Cotisation reçue") + .description("Paiement de " + cotisation.getMontantPaye() + " " + cotisation.getCodeDevise() + " reçu") + .userName(cotisation.getMembre().getNomComplet()) + .timestamp(cotisation.getDatePaiement() != null ? cotisation.getDatePaiement() : cotisation.getDateCreation()) + .userAvatar(null) + .actionUrl("/contributions/" + cotisation.getId()) + .build()); + } + } + + // Trier par timestamp décroissant et limiter + return activities.stream() + .sorted(Comparator.comparing(RecentActivityDTO::getTimestamp).reversed()) + .limit(limit) + .collect(Collectors.toList()); + } + + @Override + public List getUpcomingEvents(String organizationId, String userId, int limit) { + LOG.infof("Récupération de %d événements à venir pour org: %s et user: %s", limit, organizationId, userId); + + UUID orgId = UUID.fromString(organizationId); + + List evenements = evenementRepository.findEvenementsAVenir( + Page.of(0, limit), Sort.by("dateDebut", Sort.Direction.Ascending)); + + return evenements.stream() + .filter(e -> e.getOrganisation() == null || e.getOrganisation().getId().equals(orgId)) + .map(this::convertToUpcomingEventDTO) + .limit(limit) + .collect(Collectors.toList()); + } + + private UpcomingEventDTO convertToUpcomingEventDTO(Evenement evenement) { + return UpcomingEventDTO.builder() + .id(evenement.getId().toString()) + .title(evenement.getTitre()) + .description(evenement.getDescription()) + .startDate(evenement.getDateDebut()) + .endDate(evenement.getDateFin()) + .location(evenement.getLieu()) + .maxParticipants(evenement.getCapaciteMax()) + .currentParticipants(evenement.getNombreInscrits()) + .status(evenement.getStatut() != null ? evenement.getStatut().name() : "PLANIFIE") + .imageUrl(null) + .tags(Collections.emptyList()) + .build(); + } + + private BigDecimal calculateTotalContributionAmount(UUID organisationId) { + TypedQuery query = cotisationRepository.getEntityManager().createQuery( + "SELECT COALESCE(SUM(c.montantDu), 0) FROM Cotisation c WHERE c.membre.organisation.id = :organisationId", + BigDecimal.class); + query.setParameter("organisationId", organisationId); + BigDecimal result = query.getSingleResult(); + return result != null ? result : BigDecimal.ZERO; + } + + private Map getUserPreferences(String userId) { + Map preferences = new HashMap<>(); + preferences.put("theme", "royal_teal"); + preferences.put("language", "fr"); + preferences.put("notifications", true); + preferences.put("autoRefresh", true); + preferences.put("refreshInterval", 300); + return preferences; + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/DemandeAideService.java b/src/main/java/dev/lions/unionflow/server/service/DemandeAideService.java new file mode 100644 index 0000000..0e977fb --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/DemandeAideService.java @@ -0,0 +1,400 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO; +import dev.lions.unionflow.server.api.dto.solidarite.HistoriqueStatutDTO; +import dev.lions.unionflow.server.api.enums.solidarite.PrioriteAide; +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** + * Service spécialisé pour la gestion des demandes d'aide + * + *

Ce service gère le cycle de vie complet des demandes d'aide : création, validation, + * changements de statut, recherche et suivi. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-16 + */ +@ApplicationScoped +public class DemandeAideService { + + private static final Logger LOG = Logger.getLogger(DemandeAideService.class); + + // Cache en mémoire pour les demandes fréquemment consultées + private final Map cacheDemandesRecentes = new HashMap<>(); + private final Map cacheTimestamps = new HashMap<>(); + private static final long CACHE_DURATION_MINUTES = 15; + + // === OPÉRATIONS CRUD === + + /** + * Crée une nouvelle demande d'aide + * + * @param demandeDTO La demande à créer + * @return La demande créée avec ID généré + */ + @Transactional + public DemandeAideDTO creerDemande(@Valid DemandeAideDTO demandeDTO) { + LOG.infof("Création d'une nouvelle demande d'aide: %s", demandeDTO.getTitre()); + + // Génération des identifiants + demandeDTO.setId(UUID.randomUUID()); + demandeDTO.setNumeroReference(genererNumeroReference()); + + // Initialisation des dates + LocalDateTime maintenant = LocalDateTime.now(); + demandeDTO.setDateCreation(maintenant); + demandeDTO.setDateModification(maintenant); + + // Statut initial + if (demandeDTO.getStatut() == null) { + demandeDTO.setStatut(StatutAide.BROUILLON); + } + + // Priorité par défaut si non définie + if (demandeDTO.getPriorite() == null) { + demandeDTO.setPriorite(PrioriteAide.NORMALE); + } + + // Initialisation de l'historique + HistoriqueStatutDTO historiqueInitial = + HistoriqueStatutDTO.builder() + .id(UUID.randomUUID().toString()) + .ancienStatut(null) + .nouveauStatut(demandeDTO.getStatut()) + .dateChangement(maintenant) + .auteurId(demandeDTO.getMembreDemandeurId() != null ? demandeDTO.getMembreDemandeurId().toString() : null) + .motif("Création de la demande") + .estAutomatique(true) + .build(); + + demandeDTO.setHistoriqueStatuts(List.of(historiqueInitial)); + + // Calcul du score de priorité + demandeDTO.setScorePriorite(calculerScorePriorite(demandeDTO)); + + // Sauvegarde en cache + ajouterAuCache(demandeDTO); + + LOG.infof("Demande d'aide créée avec succès: %s", demandeDTO.getId()); + return demandeDTO; + } + + /** + * Met à jour une demande d'aide existante + * + * @param demandeDTO La demande à mettre à jour + * @return La demande mise à jour + */ + @Transactional + public DemandeAideDTO mettreAJour(@Valid DemandeAideDTO demandeDTO) { + LOG.infof("Mise à jour de la demande d'aide: %s", demandeDTO.getId()); + + // Vérification que la demande peut être modifiée + if (!demandeDTO.estModifiable()) { + throw new IllegalStateException("Cette demande ne peut plus être modifiée"); + } + + // Mise à jour de la date de modification + demandeDTO.setDateModification(LocalDateTime.now()); + demandeDTO.setVersion(demandeDTO.getVersion() + 1); + + // Recalcul du score de priorité + demandeDTO.setScorePriorite(calculerScorePriorite(demandeDTO)); + + // Mise à jour du cache + ajouterAuCache(demandeDTO); + + LOG.infof("Demande d'aide mise à jour avec succès: %s", demandeDTO.getId()); + return demandeDTO; + } + + /** + * Obtient une demande d'aide par son ID + * + * @param id UUID de la demande + * @return La demande trouvée + */ + public DemandeAideDTO obtenirParId(@NotNull UUID id) { + LOG.debugf("Récupération de la demande d'aide: %s", id); + + // Vérification du cache + DemandeAideDTO demandeCachee = obtenirDuCache(id); + if (demandeCachee != null) { + LOG.debugf("Demande trouvée dans le cache: %s", id); + return demandeCachee; + } + + // Simulation de récupération depuis la base de données + // Dans une vraie implémentation, ceci ferait appel au repository + DemandeAideDTO demande = simulerRecuperationBDD(id); + + if (demande != null) { + ajouterAuCache(demande); + } + + return demande; + } + + /** + * Change le statut d'une demande d'aide + * + * @param demandeId UUID de la demande + * @param nouveauStatut Nouveau statut + * @param motif Motif du changement + * @return La demande avec le nouveau statut + */ + @Transactional + public DemandeAideDTO changerStatut( + @NotNull UUID demandeId, @NotNull StatutAide nouveauStatut, String motif) { + LOG.infof("Changement de statut pour la demande %s: %s", demandeId, nouveauStatut); + + DemandeAideDTO demande = obtenirParId(demandeId); + if (demande == null) { + throw new IllegalArgumentException("Demande non trouvée: " + demandeId); + } + + StatutAide ancienStatut = demande.getStatut(); + + // Validation de la transition + if (!ancienStatut.peutTransitionnerVers(nouveauStatut)) { + throw new IllegalStateException( + String.format("Transition invalide de %s vers %s", ancienStatut, nouveauStatut)); + } + + // Mise à jour du statut + demande.setStatut(nouveauStatut); + demande.setDateModification(LocalDateTime.now()); + + // Ajout à l'historique + HistoriqueStatutDTO nouvelHistorique = + HistoriqueStatutDTO.builder() + .id(UUID.randomUUID().toString()) + .ancienStatut(ancienStatut) + .nouveauStatut(nouveauStatut) + .dateChangement(LocalDateTime.now()) + .motif(motif) + .estAutomatique(false) + .build(); + + List historique = new ArrayList<>(demande.getHistoriqueStatuts()); + historique.add(nouvelHistorique); + demande.setHistoriqueStatuts(historique); + + // Actions spécifiques selon le nouveau statut + switch (nouveauStatut) { + case SOUMISE -> demande.setDateSoumission(LocalDateTime.now()); + case APPROUVEE, APPROUVEE_PARTIELLEMENT -> demande.setDateApprobation(LocalDateTime.now()); + case VERSEE -> demande.setDateVersement(LocalDateTime.now()); + case CLOTUREE -> demande.setDateCloture(LocalDateTime.now()); + } + + // Mise à jour du cache + ajouterAuCache(demande); + + LOG.infof( + "Statut changé avec succès pour la demande %s: %s -> %s", + demandeId, ancienStatut, nouveauStatut); + return demande; + } + + // === RECHERCHE ET FILTRAGE === + + /** + * Recherche des demandes avec filtres + * + * @param filtres Map des critères de recherche + * @return Liste des demandes correspondantes + */ + public List rechercherAvecFiltres(Map filtres) { + LOG.debugf("Recherche de demandes avec filtres: %s", filtres); + + // Simulation de recherche - dans une vraie implémentation, + // ceci utiliserait des requêtes de base de données optimisées + List toutesLesDemandes = simulerRecuperationToutesLesDemandes(); + + return toutesLesDemandes.stream() + .filter(demande -> correspondAuxFiltres(demande, filtres)) + .sorted(this::comparerParPriorite) + .collect(Collectors.toList()); + } + + /** + * Obtient les demandes urgentes pour une organisation + * + * @param organisationId UUID de l'organisation + * @return Liste des demandes urgentes + */ + public List obtenirDemandesUrgentes(UUID organisationId) { + LOG.debugf("Récupération des demandes urgentes pour: %s", organisationId); + + Map filtres = + Map.of( + "organisationId", organisationId, + "priorite", List.of(PrioriteAide.CRITIQUE, PrioriteAide.URGENTE), + "statut", + List.of( + StatutAide.SOUMISE, + StatutAide.EN_ATTENTE, + StatutAide.EN_COURS_EVALUATION, + StatutAide.APPROUVEE)); + + return rechercherAvecFiltres(filtres); + } + + /** + * Obtient les demandes en retard (délai dépassé) + * + * @param organisationId ID de l'organisation + * @return Liste des demandes en retard + */ + public List obtenirDemandesEnRetard(UUID organisationId) { + LOG.debugf("Récupération des demandes en retard pour: %s", organisationId); + + return simulerRecuperationToutesLesDemandes().stream() + .filter(demande -> demande.getAssociationId().equals(organisationId)) + .filter(DemandeAideDTO::estDelaiDepasse) + .filter(demande -> !demande.estTerminee()) + .sorted(this::comparerParPriorite) + .collect(Collectors.toList()); + } + + // === MÉTHODES UTILITAIRES PRIVÉES === + + /** Génère un numéro de référence unique */ + private String genererNumeroReference() { + int annee = LocalDateTime.now().getYear(); + int numero = (int) (Math.random() * 999999) + 1; + return String.format("DA-%04d-%06d", annee, numero); + } + + /** Calcule le score de priorité d'une demande */ + private double calculerScorePriorite(DemandeAideDTO demande) { + double score = demande.getPriorite().getScorePriorite(); + + // Bonus pour type d'aide urgent + if (demande.getTypeAide().isUrgent()) { + score -= 1.0; + } + + // Bonus pour montant élevé (aide financière) + if (demande.getTypeAide().isFinancier() && demande.getMontantDemande() != null) { + if (demande.getMontantDemande().compareTo(new BigDecimal("50000")) > 0) { + score -= 0.5; + } + } + + // Malus pour ancienneté + long joursDepuisCreation = + java.time.Duration.between(demande.getDateCreation(), LocalDateTime.now()).toDays(); + if (joursDepuisCreation > 7) { + score += 0.3; + } + + return Math.max(0.1, score); + } + + /** Vérifie si une demande correspond aux filtres */ + private boolean correspondAuxFiltres(DemandeAideDTO demande, Map filtres) { + for (Map.Entry filtre : filtres.entrySet()) { + String cle = filtre.getKey(); + Object valeur = filtre.getValue(); + + switch (cle) { + case "organisationId" -> { + if (!demande.getAssociationId().equals(valeur)) return false; + } + case "typeAide" -> { + if (valeur instanceof List liste) { + if (!liste.contains(demande.getTypeAide())) return false; + } else if (!demande.getTypeAide().equals(valeur)) { + return false; + } + } + case "statut" -> { + if (valeur instanceof List liste) { + if (!liste.contains(demande.getStatut())) return false; + } else if (!demande.getStatut().equals(valeur)) { + return false; + } + } + case "priorite" -> { + if (valeur instanceof List liste) { + if (!liste.contains(demande.getPriorite())) return false; + } else if (!demande.getPriorite().equals(valeur)) { + return false; + } + } + case "demandeurId" -> { + if (!demande.getMembreDemandeurId().equals(valeur)) return false; + } + } + } + return true; + } + + /** Compare deux demandes par priorité */ + private int comparerParPriorite(DemandeAideDTO d1, DemandeAideDTO d2) { + // D'abord par score de priorité (plus bas = plus prioritaire) + int comparaisonScore = Double.compare(d1.getScorePriorite(), d2.getScorePriorite()); + if (comparaisonScore != 0) return comparaisonScore; + + // Puis par date de création (plus ancien = plus prioritaire) + return d1.getDateCreation().compareTo(d2.getDateCreation()); + } + + // === GESTION DU CACHE === + + private void ajouterAuCache(DemandeAideDTO demande) { + cacheDemandesRecentes.put(demande.getId(), demande); + cacheTimestamps.put(demande.getId(), LocalDateTime.now()); + + // Nettoyage du cache si trop volumineux + if (cacheDemandesRecentes.size() > 100) { + nettoyerCache(); + } + } + + private DemandeAideDTO obtenirDuCache(UUID id) { + LocalDateTime timestamp = cacheTimestamps.get(id); + if (timestamp == null) return null; + + // Vérification de l'expiration + if (LocalDateTime.now().minusMinutes(CACHE_DURATION_MINUTES).isAfter(timestamp)) { + cacheDemandesRecentes.remove(id); + cacheTimestamps.remove(id); + return null; + } + + return cacheDemandesRecentes.get(id); + } + + private void nettoyerCache() { + LocalDateTime limite = LocalDateTime.now().minusMinutes(CACHE_DURATION_MINUTES); + + cacheTimestamps.entrySet().removeIf(entry -> entry.getValue().isBefore(limite)); + cacheDemandesRecentes.keySet().retainAll(cacheTimestamps.keySet()); + } + + // === MÉTHODES DE SIMULATION (À REMPLACER PAR DE VRAIS REPOSITORIES) === + + private DemandeAideDTO simulerRecuperationBDD(UUID id) { + // Simulation - dans une vraie implémentation, ceci ferait appel au repository + return null; + } + + private List simulerRecuperationToutesLesDemandes() { + // Simulation - dans une vraie implémentation, ceci ferait appel au repository + return new ArrayList<>(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/DocumentService.java b/src/main/java/dev/lions/unionflow/server/service/DocumentService.java new file mode 100644 index 0000000..237df9c --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/DocumentService.java @@ -0,0 +1,311 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.document.DocumentDTO; +import dev.lions.unionflow.server.api.dto.document.PieceJointeDTO; +import dev.lions.unionflow.server.entity.*; +import dev.lions.unionflow.server.repository.*; +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** + * Service métier pour la gestion documentaire + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class DocumentService { + + private static final Logger LOG = Logger.getLogger(DocumentService.class); + + @Inject DocumentRepository documentRepository; + + @Inject PieceJointeRepository pieceJointeRepository; + + @Inject MembreRepository membreRepository; + + @Inject OrganisationRepository organisationRepository; + + @Inject CotisationRepository cotisationRepository; + + @Inject AdhesionRepository adhesionRepository; + + @Inject DemandeAideRepository demandeAideRepository; + + @Inject TransactionWaveRepository transactionWaveRepository; + + @Inject KeycloakService keycloakService; + + /** + * Crée un nouveau document + * + * @param documentDTO DTO du document à créer + * @return DTO du document créé + */ + @Transactional + public DocumentDTO creerDocument(DocumentDTO documentDTO) { + LOG.infof("Création d'un nouveau document: %s", documentDTO.getNomFichier()); + + Document document = convertToEntity(documentDTO); + document.setCreePar(keycloakService.getCurrentUserEmail()); + + documentRepository.persist(document); + LOG.infof("Document créé avec succès: ID=%s, Fichier=%s", document.getId(), document.getNomFichier()); + + return convertToDTO(document); + } + + /** + * Trouve un document par son ID + * + * @param id ID du document + * @return DTO du document + */ + public DocumentDTO trouverParId(UUID id) { + return documentRepository + .findDocumentById(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Document non trouvé avec l'ID: " + id)); + } + + /** + * Enregistre un téléchargement de document + * + * @param id ID du document + */ + @Transactional + public void enregistrerTelechargement(UUID id) { + Document document = + documentRepository + .findDocumentById(id) + .orElseThrow(() -> new NotFoundException("Document non trouvé avec l'ID: " + id)); + + document.setNombreTelechargements( + (document.getNombreTelechargements() != null ? document.getNombreTelechargements() : 0) + 1); + document.setDateDernierTelechargement(LocalDateTime.now()); + document.setModifiePar(keycloakService.getCurrentUserEmail()); + + documentRepository.persist(document); + } + + /** + * Crée une pièce jointe + * + * @param pieceJointeDTO DTO de la pièce jointe à créer + * @return DTO de la pièce jointe créée + */ + @Transactional + public PieceJointeDTO creerPieceJointe(PieceJointeDTO pieceJointeDTO) { + LOG.infof("Création d'une nouvelle pièce jointe"); + + PieceJointe pieceJointe = convertToEntity(pieceJointeDTO); + + // Vérifier qu'une seule relation est renseignée + if (!pieceJointe.isValide()) { + throw new IllegalArgumentException("Une seule relation doit être renseignée pour une pièce jointe"); + } + + pieceJointe.setCreePar(keycloakService.getCurrentUserEmail()); + pieceJointeRepository.persist(pieceJointe); + + LOG.infof("Pièce jointe créée avec succès: ID=%s", pieceJointe.getId()); + return convertToDTO(pieceJointe); + } + + /** + * Liste toutes les pièces jointes d'un document + * + * @param documentId ID du document + * @return Liste des pièces jointes + */ + public List listerPiecesJointesParDocument(UUID documentId) { + return pieceJointeRepository.findByDocumentId(documentId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + // ======================================== + // MÉTHODES PRIVÉES + // ======================================== + + /** Convertit une entité Document en DTO */ + private DocumentDTO convertToDTO(Document document) { + if (document == null) { + return null; + } + + DocumentDTO dto = new DocumentDTO(); + dto.setId(document.getId()); + dto.setNomFichier(document.getNomFichier()); + dto.setNomOriginal(document.getNomOriginal()); + dto.setCheminStockage(document.getCheminStockage()); + dto.setTypeMime(document.getTypeMime()); + dto.setTailleOctets(document.getTailleOctets()); + dto.setTypeDocument(document.getTypeDocument()); + dto.setHashMd5(document.getHashMd5()); + dto.setHashSha256(document.getHashSha256()); + dto.setDescription(document.getDescription()); + dto.setNombreTelechargements(document.getNombreTelechargements()); + dto.setTailleFormatee(document.getTailleFormatee()); + dto.setDateCreation(document.getDateCreation()); + dto.setDateModification(document.getDateModification()); + dto.setActif(document.getActif()); + + return dto; + } + + /** Convertit un DTO en entité Document */ + private Document convertToEntity(DocumentDTO dto) { + if (dto == null) { + return null; + } + + Document document = new Document(); + document.setNomFichier(dto.getNomFichier()); + document.setNomOriginal(dto.getNomOriginal()); + document.setCheminStockage(dto.getCheminStockage()); + document.setTypeMime(dto.getTypeMime()); + document.setTailleOctets(dto.getTailleOctets()); + document.setTypeDocument(dto.getTypeDocument() != null ? dto.getTypeDocument() : dev.lions.unionflow.server.api.enums.document.TypeDocument.AUTRE); + document.setHashMd5(dto.getHashMd5()); + document.setHashSha256(dto.getHashSha256()); + document.setDescription(dto.getDescription()); + document.setNombreTelechargements(dto.getNombreTelechargements() != null ? dto.getNombreTelechargements() : 0); + + return document; + } + + /** Convertit une entité PieceJointe en DTO */ + private PieceJointeDTO convertToDTO(PieceJointe pieceJointe) { + if (pieceJointe == null) { + return null; + } + + PieceJointeDTO dto = new PieceJointeDTO(); + dto.setId(pieceJointe.getId()); + dto.setOrdre(pieceJointe.getOrdre()); + dto.setLibelle(pieceJointe.getLibelle()); + dto.setCommentaire(pieceJointe.getCommentaire()); + + if (pieceJointe.getDocument() != null) { + dto.setDocumentId(pieceJointe.getDocument().getId()); + } + if (pieceJointe.getMembre() != null) { + dto.setMembreId(pieceJointe.getMembre().getId()); + } + if (pieceJointe.getOrganisation() != null) { + dto.setOrganisationId(pieceJointe.getOrganisation().getId()); + } + if (pieceJointe.getCotisation() != null) { + dto.setCotisationId(pieceJointe.getCotisation().getId()); + } + if (pieceJointe.getAdhesion() != null) { + dto.setAdhesionId(pieceJointe.getAdhesion().getId()); + } + if (pieceJointe.getDemandeAide() != null) { + dto.setDemandeAideId(pieceJointe.getDemandeAide().getId()); + } + if (pieceJointe.getTransactionWave() != null) { + dto.setTransactionWaveId(pieceJointe.getTransactionWave().getId()); + } + + dto.setDateCreation(pieceJointe.getDateCreation()); + dto.setDateModification(pieceJointe.getDateModification()); + dto.setActif(pieceJointe.getActif()); + + return dto; + } + + /** Convertit un DTO en entité PieceJointe */ + private PieceJointe convertToEntity(PieceJointeDTO dto) { + if (dto == null) { + return null; + } + + PieceJointe pieceJointe = new PieceJointe(); + pieceJointe.setOrdre(dto.getOrdre() != null ? dto.getOrdre() : 1); + pieceJointe.setLibelle(dto.getLibelle()); + pieceJointe.setCommentaire(dto.getCommentaire()); + + // Relation Document + if (dto.getDocumentId() != null) { + Document document = + documentRepository + .findDocumentById(dto.getDocumentId()) + .orElseThrow(() -> new NotFoundException("Document non trouvé avec l'ID: " + dto.getDocumentId())); + pieceJointe.setDocument(document); + } + + // Relations flexibles (une seule doit être renseignée) + if (dto.getMembreId() != null) { + Membre membre = + membreRepository + .findByIdOptional(dto.getMembreId()) + .orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId())); + pieceJointe.setMembre(membre); + } + + if (dto.getOrganisationId() != null) { + Organisation org = + organisationRepository + .findByIdOptional(dto.getOrganisationId()) + .orElseThrow( + () -> + new NotFoundException( + "Organisation non trouvée avec l'ID: " + dto.getOrganisationId())); + pieceJointe.setOrganisation(org); + } + + if (dto.getCotisationId() != null) { + Cotisation cotisation = + cotisationRepository + .findByIdOptional(dto.getCotisationId()) + .orElseThrow( + () -> new NotFoundException("Cotisation non trouvée avec l'ID: " + dto.getCotisationId())); + pieceJointe.setCotisation(cotisation); + } + + if (dto.getAdhesionId() != null) { + Adhesion adhesion = + adhesionRepository + .findByIdOptional(dto.getAdhesionId()) + .orElseThrow( + () -> new NotFoundException("Adhésion non trouvée avec l'ID: " + dto.getAdhesionId())); + pieceJointe.setAdhesion(adhesion); + } + + if (dto.getDemandeAideId() != null) { + DemandeAide demandeAide = + demandeAideRepository + .findByIdOptional(dto.getDemandeAideId()) + .orElseThrow( + () -> + new NotFoundException( + "Demande d'aide non trouvée avec l'ID: " + dto.getDemandeAideId())); + pieceJointe.setDemandeAide(demandeAide); + } + + if (dto.getTransactionWaveId() != null) { + TransactionWave transactionWave = + transactionWaveRepository + .findTransactionWaveById(dto.getTransactionWaveId()) + .orElseThrow( + () -> + new NotFoundException( + "Transaction Wave non trouvée avec l'ID: " + dto.getTransactionWaveId())); + pieceJointe.setTransactionWave(transactionWave); + } + + return pieceJointe; + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/service/EvenementService.java b/src/main/java/dev/lions/unionflow/server/service/EvenementService.java new file mode 100644 index 0000000..209e4be --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/EvenementService.java @@ -0,0 +1,340 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.entity.Evenement; +import dev.lions.unionflow.server.entity.Evenement.StatutEvenement; +import dev.lions.unionflow.server.entity.Evenement.TypeEvenement; +import dev.lions.unionflow.server.repository.EvenementRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import dev.lions.unionflow.server.service.KeycloakService; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Service métier pour la gestion des événements Version simplifiée pour tester les imports et + * Lombok + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@ApplicationScoped +public class EvenementService { + + private static final Logger LOG = Logger.getLogger(EvenementService.class); + + @Inject EvenementRepository evenementRepository; + + @Inject MembreRepository membreRepository; + + @Inject OrganisationRepository organisationRepository; + + @Inject KeycloakService keycloakService; + + /** + * Crée un nouvel événement + * + * @param evenement l'événement à créer + * @return l'événement créé + * @throws IllegalArgumentException si les données sont invalides + */ + @Transactional + public Evenement creerEvenement(Evenement evenement) { + LOG.infof("Création événement: %s", evenement.getTitre()); + + // Validation des données + validerEvenement(evenement); + + // Vérifier l'unicité du titre dans l'organisation + if (evenement.getOrganisation() != null) { + Optional existant = evenementRepository.findByTitre(evenement.getTitre()); + if (existant.isPresent() + && existant.get().getOrganisation().getId().equals(evenement.getOrganisation().getId())) { + throw new IllegalArgumentException( + "Un événement avec ce titre existe déjà dans cette organisation"); + } + } + + // Métadonnées de création + evenement.setCreePar(keycloakService.getCurrentUserEmail()); + + // Valeurs par défaut + if (evenement.getStatut() == null) { + evenement.setStatut(StatutEvenement.PLANIFIE); + } + if (evenement.getActif() == null) { + evenement.setActif(true); + } + if (evenement.getVisiblePublic() == null) { + evenement.setVisiblePublic(true); + } + if (evenement.getInscriptionRequise() == null) { + evenement.setInscriptionRequise(true); + } + + evenementRepository.persist(evenement); + + LOG.infof("Événement créé avec succès: ID=%s, Titre=%s", evenement.getId(), evenement.getTitre()); + return evenement; + } + + /** + * Met à jour un événement existant + * + * @param id l'UUID de l'événement + * @param evenementMisAJour les nouvelles données + * @return l'événement mis à jour + * @throws IllegalArgumentException si l'événement n'existe pas + */ + @Transactional + public Evenement mettreAJourEvenement(UUID id, Evenement evenementMisAJour) { + LOG.infof("Mise à jour événement ID: %s", id); + + Evenement evenementExistant = + evenementRepository + .findByIdOptional(id) + .orElseThrow( + () -> new IllegalArgumentException("Événement non trouvé avec l'ID: " + id)); + + // Vérifier les permissions + if (!peutModifierEvenement(evenementExistant)) { + throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement"); + } + + // Validation des nouvelles données + validerEvenement(evenementMisAJour); + + // Mise à jour des champs + evenementExistant.setTitre(evenementMisAJour.getTitre()); + evenementExistant.setDescription(evenementMisAJour.getDescription()); + evenementExistant.setDateDebut(evenementMisAJour.getDateDebut()); + evenementExistant.setDateFin(evenementMisAJour.getDateFin()); + evenementExistant.setLieu(evenementMisAJour.getLieu()); + evenementExistant.setAdresse(evenementMisAJour.getAdresse()); + evenementExistant.setTypeEvenement(evenementMisAJour.getTypeEvenement()); + evenementExistant.setCapaciteMax(evenementMisAJour.getCapaciteMax()); + evenementExistant.setPrix(evenementMisAJour.getPrix()); + evenementExistant.setInscriptionRequise(evenementMisAJour.getInscriptionRequise()); + evenementExistant.setDateLimiteInscription(evenementMisAJour.getDateLimiteInscription()); + evenementExistant.setInstructionsParticulieres( + evenementMisAJour.getInstructionsParticulieres()); + evenementExistant.setContactOrganisateur(evenementMisAJour.getContactOrganisateur()); + evenementExistant.setMaterielRequis(evenementMisAJour.getMaterielRequis()); + evenementExistant.setVisiblePublic(evenementMisAJour.getVisiblePublic()); + + // Métadonnées de modification + evenementExistant.setModifiePar(keycloakService.getCurrentUserEmail()); + + evenementRepository.update(evenementExistant); + + LOG.infof("Événement mis à jour avec succès: ID=%s", id); + return evenementExistant; + } + + /** Trouve un événement par ID */ + public Optional trouverParId(UUID id) { + return evenementRepository.findByIdOptional(id); + } + + /** Liste tous les événements actifs avec pagination */ + public List listerEvenementsActifs(Page page, Sort sort) { + return evenementRepository.findAllActifs(page, sort); + } + + /** Liste les événements à venir */ + public List listerEvenementsAVenir(Page page, Sort sort) { + return evenementRepository.findEvenementsAVenir(page, sort); + } + + /** Liste les événements publics */ + public List listerEvenementsPublics(Page page, Sort sort) { + return evenementRepository.findEvenementsPublics(page, sort); + } + + /** Recherche d'événements par terme */ + public List rechercherEvenements(String terme, Page page, Sort sort) { + return evenementRepository.rechercheAvancee( + terme, null, null, null, null, null, null, null, null, null, page, sort); + } + + /** Liste les événements par type */ + public List listerParType(TypeEvenement type, Page page, Sort sort) { + return evenementRepository.findByType(type, page, sort); + } + + /** + * Supprime logiquement un événement + * + * @param id l'UUID de l'événement à supprimer + * @throws IllegalArgumentException si l'événement n'existe pas + */ + @Transactional + public void supprimerEvenement(UUID id) { + LOG.infof("Suppression événement ID: %s", id); + + Evenement evenement = + evenementRepository + .findByIdOptional(id) + .orElseThrow( + () -> new IllegalArgumentException("Événement non trouvé avec l'ID: " + id)); + + // Vérifier les permissions + if (!peutModifierEvenement(evenement)) { + throw new SecurityException("Vous n'avez pas les permissions pour supprimer cet événement"); + } + + // Vérifier s'il y a des inscriptions + if (evenement.getNombreInscrits() > 0) { + throw new IllegalStateException("Impossible de supprimer un événement avec des inscriptions"); + } + + // Suppression logique + evenement.setActif(false); + evenement.setModifiePar(keycloakService.getCurrentUserEmail()); + + evenementRepository.update(evenement); + + LOG.infof("Événement supprimé avec succès: ID=%s", id); + } + + /** + * Change le statut d'un événement + * + * @param id l'UUID de l'événement + * @param nouveauStatut le nouveau statut + * @return l'événement mis à jour + */ + @Transactional + public Evenement changerStatut(UUID id, StatutEvenement nouveauStatut) { + LOG.infof("Changement statut événement ID: %s vers %s", id, nouveauStatut); + + Evenement evenement = + evenementRepository + .findByIdOptional(id) + .orElseThrow( + () -> new IllegalArgumentException("Événement non trouvé avec l'ID: " + id)); + + // Vérifier les permissions + if (!peutModifierEvenement(evenement)) { + throw new SecurityException("Vous n'avez pas les permissions pour modifier cet événement"); + } + + // Valider le changement de statut + validerChangementStatut(evenement.getStatut(), nouveauStatut); + + evenement.setStatut(nouveauStatut); + evenement.setModifiePar(keycloakService.getCurrentUserEmail()); + + evenementRepository.update(evenement); + + LOG.infof("Statut événement changé avec succès: ID=%s, Nouveau statut=%s", id, nouveauStatut); + return evenement; + } + + /** + * Compte le nombre total d'événements + * + * @return le nombre total d'événements + */ + public long countEvenements() { + return evenementRepository.count(); + } + + /** + * Compte le nombre d'événements actifs + * + * @return le nombre d'événements actifs + */ + public long countEvenementsActifs() { + return evenementRepository.countActifs(); + } + + /** + * Obtient les statistiques des événements + * + * @return les statistiques sous forme de Map + */ + public Map obtenirStatistiques() { + Map statsBase = evenementRepository.getStatistiques(); + + long total = statsBase.getOrDefault("total", 0L); + long actifs = statsBase.getOrDefault("actifs", 0L); + long aVenir = statsBase.getOrDefault("aVenir", 0L); + long enCours = statsBase.getOrDefault("enCours", 0L); + + Map result = new java.util.HashMap<>(); + result.put("total", total); + result.put("actifs", actifs); + result.put("aVenir", aVenir); + result.put("enCours", enCours); + result.put("passes", statsBase.getOrDefault("passes", 0L)); + result.put("publics", statsBase.getOrDefault("publics", 0L)); + result.put("avecInscription", statsBase.getOrDefault("avecInscription", 0L)); + result.put("tauxActivite", total > 0 ? (actifs * 100.0 / total) : 0.0); + result.put("tauxEvenementsAVenir", total > 0 ? (aVenir * 100.0 / total) : 0.0); + result.put("tauxEvenementsEnCours", total > 0 ? (enCours * 100.0 / total) : 0.0); + result.put("timestamp", LocalDateTime.now()); + return result; + } + + // Méthodes privées de validation et permissions + + /** Valide les données d'un événement */ + private void validerEvenement(Evenement evenement) { + if (evenement.getTitre() == null || evenement.getTitre().trim().isEmpty()) { + throw new IllegalArgumentException("Le titre de l'événement est obligatoire"); + } + + if (evenement.getDateDebut() == null) { + throw new IllegalArgumentException("La date de début est obligatoire"); + } + + if (evenement.getDateDebut().isBefore(LocalDateTime.now().minusHours(1))) { + throw new IllegalArgumentException("La date de début ne peut pas être dans le passé"); + } + + if (evenement.getDateFin() != null + && evenement.getDateFin().isBefore(evenement.getDateDebut())) { + throw new IllegalArgumentException( + "La date de fin ne peut pas être antérieure à la date de début"); + } + + if (evenement.getCapaciteMax() != null && evenement.getCapaciteMax() <= 0) { + throw new IllegalArgumentException("La capacité maximale doit être positive"); + } + + if (evenement.getPrix() != null + && evenement.getPrix().compareTo(java.math.BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Le prix ne peut pas être négatif"); + } + } + + /** Valide un changement de statut */ + private void validerChangementStatut( + StatutEvenement statutActuel, StatutEvenement nouveauStatut) { + // Règles de transition simplifiées pour la version mobile + if (statutActuel == StatutEvenement.TERMINE || statutActuel == StatutEvenement.ANNULE) { + throw new IllegalArgumentException( + "Impossible de changer le statut d'un événement terminé ou annulé"); + } + } + + /** Vérifie les permissions de modification pour l'application mobile */ + private boolean peutModifierEvenement(Evenement evenement) { + if (keycloakService.hasRole("ADMIN") || keycloakService.hasRole("ORGANISATEUR_EVENEMENT")) { + return true; + } + + String utilisateurActuel = keycloakService.getCurrentUserEmail(); + return utilisateurActuel != null && utilisateurActuel.equals(evenement.getCreePar()); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/ExportService.java b/src/main/java/dev/lions/unionflow/server/service/ExportService.java new file mode 100644 index 0000000..659e86d --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/ExportService.java @@ -0,0 +1,237 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.finance.CotisationDTO; +import dev.lions.unionflow.server.entity.Cotisation; +import dev.lions.unionflow.server.repository.CotisationRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.jboss.logging.Logger; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * Service d'export des données en Excel et PDF + * + * @author UnionFlow Team + * @version 1.0 + */ +@ApplicationScoped +public class ExportService { + + private static final Logger LOG = Logger.getLogger(ExportService.class); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"); + + @Inject + CotisationRepository cotisationRepository; + + @Inject + CotisationService cotisationService; + + /** + * Exporte les cotisations en format CSV (compatible Excel) + */ + public byte[] exporterCotisationsCSV(List cotisationIds) { + LOG.infof("Export CSV de %d cotisations", cotisationIds.size()); + + StringBuilder csv = new StringBuilder(); + csv.append("Numéro Référence;Membre;Type;Montant Dû;Montant Payé;Statut;Date Échéance;Date Paiement;Méthode Paiement\n"); + + for (UUID id : cotisationIds) { + Optional cotisationOpt = cotisationRepository.findByIdOptional(id); + if (cotisationOpt.isPresent()) { + Cotisation c = cotisationOpt.get(); + String nomMembre = c.getMembre() != null + ? c.getMembre().getNom() + " " + c.getMembre().getPrenom() + : ""; + csv.append(String.format("%s;%s;%s;%s;%s;%s;%s;%s;%s\n", + c.getNumeroReference() != null ? c.getNumeroReference() : "", + nomMembre, + c.getTypeCotisation() != null ? c.getTypeCotisation() : "", + c.getMontantDu() != null ? c.getMontantDu().toString() : "0", + c.getMontantPaye() != null ? c.getMontantPaye().toString() : "0", + c.getStatut() != null ? c.getStatut() : "", + c.getDateEcheance() != null ? c.getDateEcheance().format(DATE_FORMATTER) : "", + c.getDatePaiement() != null ? c.getDatePaiement().format(DATETIME_FORMATTER) : "", + c.getMethodePaiement() != null ? c.getMethodePaiement() : "" + )); + } + } + + return csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + /** + * Exporte toutes les cotisations filtrées en CSV + */ + public byte[] exporterToutesCotisationsCSV(String statut, String type, UUID associationId) { + LOG.info("Export CSV de toutes les cotisations"); + + List cotisations = cotisationRepository.listAll(); + + // Filtrer + if (statut != null && !statut.isEmpty()) { + cotisations = cotisations.stream() + .filter(c -> c.getStatut() != null && c.getStatut().equals(statut)) + .toList(); + } + if (type != null && !type.isEmpty()) { + cotisations = cotisations.stream() + .filter(c -> c.getTypeCotisation() != null && c.getTypeCotisation().equals(type)) + .toList(); + } + // Note: le filtrage par association n'est pas disponible car Membre n'a pas de lien direct + // avec Association dans cette version du modèle + + List ids = cotisations.stream().map(Cotisation::getId).toList(); + return exporterCotisationsCSV(ids); + } + + /** + * Génère un reçu de paiement en format texte (pour impression) + */ + public byte[] genererRecuPaiement(UUID cotisationId) { + LOG.infof("Génération reçu pour cotisation: %s", cotisationId); + + Optional cotisationOpt = cotisationRepository.findByIdOptional(cotisationId); + if (cotisationOpt.isEmpty()) { + return "Cotisation non trouvée".getBytes(); + } + + Cotisation c = cotisationOpt.get(); + + StringBuilder recu = new StringBuilder(); + recu.append("═══════════════════════════════════════════════════════════════\n"); + recu.append(" REÇU DE PAIEMENT\n"); + recu.append("═══════════════════════════════════════════════════════════════\n\n"); + + recu.append("Numéro de reçu : ").append(c.getNumeroReference()).append("\n"); + recu.append("Date : ").append(LocalDateTime.now().format(DATETIME_FORMATTER)).append("\n\n"); + + recu.append("───────────────────────────────────────────────────────────────\n"); + recu.append(" INFORMATIONS MEMBRE\n"); + recu.append("───────────────────────────────────────────────────────────────\n"); + + if (c.getMembre() != null) { + recu.append("Nom : ").append(c.getMembre().getNom()).append(" ").append(c.getMembre().getPrenom()).append("\n"); + recu.append("Numéro membre : ").append(c.getMembre().getNumeroMembre()).append("\n"); + } + + recu.append("\n───────────────────────────────────────────────────────────────\n"); + recu.append(" DÉTAILS DU PAIEMENT\n"); + recu.append("───────────────────────────────────────────────────────────────\n"); + + recu.append("Type cotisation : ").append(c.getTypeCotisation() != null ? c.getTypeCotisation() : "").append("\n"); + recu.append("Période : ").append(c.getPeriode() != null ? c.getPeriode() : "").append("\n"); + recu.append("Montant dû : ").append(formatMontant(c.getMontantDu())).append("\n"); + recu.append("Montant payé : ").append(formatMontant(c.getMontantPaye())).append("\n"); + recu.append("Mode de paiement : ").append(c.getMethodePaiement() != null ? c.getMethodePaiement() : "").append("\n"); + recu.append("Date de paiement : ").append(c.getDatePaiement() != null ? c.getDatePaiement().format(DATETIME_FORMATTER) : "").append("\n"); + recu.append("Statut : ").append(c.getStatut() != null ? c.getStatut() : "").append("\n"); + + recu.append("\n═══════════════════════════════════════════════════════════════\n"); + recu.append(" Ce document fait foi de paiement de cotisation\n"); + recu.append(" Merci de votre confiance !\n"); + recu.append("═══════════════════════════════════════════════════════════════\n"); + + return recu.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + /** + * Génère plusieurs reçus de paiement + */ + public byte[] genererRecusGroupes(List cotisationIds) { + LOG.infof("Génération de %d reçus groupés", cotisationIds.size()); + + StringBuilder allRecus = new StringBuilder(); + for (int i = 0; i < cotisationIds.size(); i++) { + byte[] recu = genererRecuPaiement(cotisationIds.get(i)); + allRecus.append(new String(recu, java.nio.charset.StandardCharsets.UTF_8)); + if (i < cotisationIds.size() - 1) { + allRecus.append("\n\n════════════════════════ PAGE SUIVANTE ════════════════════════\n\n"); + } + } + + return allRecus.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + /** + * Génère un rapport mensuel + */ + public byte[] genererRapportMensuel(int annee, int mois, UUID associationId) { + LOG.infof("Génération rapport mensuel: %d/%d", mois, annee); + + List cotisations = cotisationRepository.listAll(); + + // Filtrer par mois/année et association + LocalDate debut = LocalDate.of(annee, mois, 1); + LocalDate fin = debut.plusMonths(1).minusDays(1); + + cotisations = cotisations.stream() + .filter(c -> { + if (c.getDateCreation() == null) return false; + LocalDate dateCot = c.getDateCreation().toLocalDate(); + return !dateCot.isBefore(debut) && !dateCot.isAfter(fin); + }) + // Note: le filtrage par association n'est pas implémenté ici + .toList(); + + // Calculer les statistiques + long total = cotisations.size(); + long payees = cotisations.stream().filter(c -> "PAYEE".equals(c.getStatut())).count(); + long enAttente = cotisations.stream().filter(c -> "EN_ATTENTE".equals(c.getStatut())).count(); + long enRetard = cotisations.stream().filter(c -> "EN_RETARD".equals(c.getStatut())).count(); + + BigDecimal montantTotal = cotisations.stream() + .map(c -> c.getMontantDu() != null ? c.getMontantDu() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal montantCollecte = cotisations.stream() + .filter(c -> "PAYEE".equals(c.getStatut()) || "PARTIELLEMENT_PAYEE".equals(c.getStatut())) + .map(c -> c.getMontantPaye() != null ? c.getMontantPaye() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + double tauxRecouvrement = montantTotal.compareTo(BigDecimal.ZERO) > 0 + ? montantCollecte.multiply(BigDecimal.valueOf(100)).divide(montantTotal, 2, java.math.RoundingMode.HALF_UP).doubleValue() + : 0; + + // Construire le rapport + StringBuilder rapport = new StringBuilder(); + rapport.append("═══════════════════════════════════════════════════════════════\n"); + rapport.append(" RAPPORT MENSUEL DES COTISATIONS\n"); + rapport.append("═══════════════════════════════════════════════════════════════\n\n"); + + rapport.append("Période : ").append(String.format("%02d/%d", mois, annee)).append("\n"); + rapport.append("Date de génération: ").append(LocalDateTime.now().format(DATETIME_FORMATTER)).append("\n\n"); + + rapport.append("───────────────────────────────────────────────────────────────\n"); + rapport.append(" RÉSUMÉ\n"); + rapport.append("───────────────────────────────────────────────────────────────\n\n"); + + rapport.append("Total cotisations : ").append(total).append("\n"); + rapport.append("Cotisations payées : ").append(payees).append("\n"); + rapport.append("Cotisations en attente: ").append(enAttente).append("\n"); + rapport.append("Cotisations en retard : ").append(enRetard).append("\n\n"); + + rapport.append("───────────────────────────────────────────────────────────────\n"); + rapport.append(" FINANCIER\n"); + rapport.append("───────────────────────────────────────────────────────────────\n\n"); + + rapport.append("Montant total attendu : ").append(formatMontant(montantTotal)).append("\n"); + rapport.append("Montant collecté : ").append(formatMontant(montantCollecte)).append("\n"); + rapport.append("Taux de recouvrement : ").append(String.format("%.1f%%", tauxRecouvrement)).append("\n\n"); + + rapport.append("═══════════════════════════════════════════════════════════════\n"); + + return rapport.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + private String formatMontant(BigDecimal montant) { + if (montant == null) return "0 FCFA"; + return String.format("%,.0f FCFA", montant.doubleValue()); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/KPICalculatorService.java b/src/main/java/dev/lions/unionflow/server/service/KPICalculatorService.java new file mode 100644 index 0000000..c99280b --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/KPICalculatorService.java @@ -0,0 +1,363 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique; +import dev.lions.unionflow.server.repository.CotisationRepository; +import dev.lions.unionflow.server.repository.DemandeAideRepository; +import dev.lions.unionflow.server.repository.EvenementRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; + +/** + * Service spécialisé dans le calcul des KPI (Key Performance Indicators) + * + *

Ce service fournit des méthodes optimisées pour calculer les indicateurs de performance clés + * de l'application UnionFlow. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-16 + */ +@ApplicationScoped +@Slf4j +public class KPICalculatorService { + + @Inject MembreRepository membreRepository; + + @Inject CotisationRepository cotisationRepository; + + @Inject EvenementRepository evenementRepository; + + @Inject DemandeAideRepository demandeAideRepository; + + /** + * Calcule tous les KPI principaux pour une organisation + * + * @param organisationId L'ID de l'organisation + * @param dateDebut Date de début de la période + * @param dateFin Date de fin de la période + * @return Map contenant tous les KPI calculés + */ + public Map calculerTousLesKPI( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + log.info( + "Calcul de tous les KPI pour l'organisation {} sur la période {} - {}", + organisationId, + dateDebut, + dateFin); + + Map kpis = new HashMap<>(); + + // KPI Membres + kpis.put( + TypeMetrique.NOMBRE_MEMBRES_ACTIFS, + calculerKPIMembresActifs(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.NOMBRE_MEMBRES_INACTIFS, + calculerKPIMembresInactifs(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.TAUX_CROISSANCE_MEMBRES, + calculerKPITauxCroissanceMembres(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.MOYENNE_AGE_MEMBRES, + calculerKPIMoyenneAgeMembres(organisationId, dateDebut, dateFin)); + + // KPI Financiers + kpis.put( + TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, + calculerKPITotalCotisations(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.COTISATIONS_EN_ATTENTE, + calculerKPICotisationsEnAttente(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.TAUX_RECOUVREMENT_COTISATIONS, + calculerKPITauxRecouvrement(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.MOYENNE_COTISATION_MEMBRE, + calculerKPIMoyenneCotisationMembre(organisationId, dateDebut, dateFin)); + + // KPI Événements + kpis.put( + TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, + calculerKPINombreEvenements(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.TAUX_PARTICIPATION_EVENEMENTS, + calculerKPITauxParticipation(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.MOYENNE_PARTICIPANTS_EVENEMENT, + calculerKPIMoyenneParticipants(organisationId, dateDebut, dateFin)); + + // KPI Solidarité + kpis.put( + TypeMetrique.NOMBRE_DEMANDES_AIDE, + calculerKPINombreDemandesAide(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.MONTANT_AIDES_ACCORDEES, + calculerKPIMontantAides(organisationId, dateDebut, dateFin)); + kpis.put( + TypeMetrique.TAUX_APPROBATION_AIDES, + calculerKPITauxApprobationAides(organisationId, dateDebut, dateFin)); + + log.info("Calcul terminé : {} KPI calculés", kpis.size()); + return kpis; + } + + /** + * Calcule le KPI de performance globale de l'organisation + * + * @param organisationId L'ID de l'organisation + * @param dateDebut Date de début de la période + * @param dateFin Date de fin de la période + * @return Score de performance global (0-100) + */ + public BigDecimal calculerKPIPerformanceGlobale( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + log.info("Calcul du KPI de performance globale pour l'organisation {}", organisationId); + + Map kpis = calculerTousLesKPI(organisationId, dateDebut, dateFin); + + // Pondération des différents KPI pour le score global + BigDecimal scoreMembers = calculerScoreMembres(kpis).multiply(new BigDecimal("0.30")); // 30% + BigDecimal scoreFinancier = + calculerScoreFinancier(kpis).multiply(new BigDecimal("0.35")); // 35% + BigDecimal scoreEvenements = + calculerScoreEvenements(kpis).multiply(new BigDecimal("0.20")); // 20% + BigDecimal scoreSolidarite = + calculerScoreSolidarite(kpis).multiply(new BigDecimal("0.15")); // 15% + + BigDecimal scoreGlobal = + scoreMembers.add(scoreFinancier).add(scoreEvenements).add(scoreSolidarite); + + log.info("Score de performance globale calculé : {}", scoreGlobal); + return scoreGlobal.setScale(1, RoundingMode.HALF_UP); + } + + /** + * Calcule les KPI de comparaison avec la période précédente + * + * @param organisationId L'ID de l'organisation + * @param dateDebut Date de début de la période actuelle + * @param dateFin Date de fin de la période actuelle + * @return Map des évolutions en pourcentage + */ + public Map calculerEvolutionsKPI( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + log.info("Calcul des évolutions KPI pour l'organisation {}", organisationId); + + // Période actuelle + Map kpisActuels = + calculerTousLesKPI(organisationId, dateDebut, dateFin); + + // Période précédente (même durée, décalée) + long dureeJours = java.time.Duration.between(dateDebut, dateFin).toDays(); + LocalDateTime dateDebutPrecedente = dateDebut.minusDays(dureeJours); + LocalDateTime dateFinPrecedente = dateFin.minusDays(dureeJours); + Map kpisPrecedents = + calculerTousLesKPI(organisationId, dateDebutPrecedente, dateFinPrecedente); + + Map evolutions = new HashMap<>(); + + for (TypeMetrique typeMetrique : kpisActuels.keySet()) { + BigDecimal valeurActuelle = kpisActuels.get(typeMetrique); + BigDecimal valeurPrecedente = kpisPrecedents.get(typeMetrique); + + BigDecimal evolution = calculerPourcentageEvolution(valeurActuelle, valeurPrecedente); + evolutions.put(typeMetrique, evolution); + } + + return evolutions; + } + + // === MÉTHODES PRIVÉES DE CALCUL DES KPI === + + private BigDecimal calculerKPIMembresActifs( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long count = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin); + return new BigDecimal(count); + } + + private BigDecimal calculerKPIMembresInactifs( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long count = membreRepository.countMembresInactifs(organisationId, dateDebut, dateFin); + return new BigDecimal(count); + } + + private BigDecimal calculerKPITauxCroissanceMembres( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long membresActuels = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin); + Long membresPrecedents = + membreRepository.countMembresActifs( + organisationId, dateDebut.minusMonths(1), dateFin.minusMonths(1)); + + return calculerTauxCroissance( + new BigDecimal(membresActuels), new BigDecimal(membresPrecedents)); + } + + private BigDecimal calculerKPIMoyenneAgeMembres( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Double moyenneAge = membreRepository.calculerMoyenneAge(organisationId, dateDebut, dateFin); + return moyenneAge != null + ? new BigDecimal(moyenneAge).setScale(1, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + } + + private BigDecimal calculerKPITotalCotisations( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal total = cotisationRepository.sumMontantsPayes(organisationId, dateDebut, dateFin); + return total != null ? total : BigDecimal.ZERO; + } + + private BigDecimal calculerKPICotisationsEnAttente( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal total = + cotisationRepository.sumMontantsEnAttente(organisationId, dateDebut, dateFin); + return total != null ? total : BigDecimal.ZERO; + } + + private BigDecimal calculerKPITauxRecouvrement( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal collectees = calculerKPITotalCotisations(organisationId, dateDebut, dateFin); + BigDecimal enAttente = calculerKPICotisationsEnAttente(organisationId, dateDebut, dateFin); + BigDecimal total = collectees.add(enAttente); + + if (total.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO; + + return collectees.divide(total, 4, RoundingMode.HALF_UP).multiply(new BigDecimal("100")); + } + + private BigDecimal calculerKPIMoyenneCotisationMembre( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal total = calculerKPITotalCotisations(organisationId, dateDebut, dateFin); + Long nombreMembres = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin); + + if (nombreMembres == 0) return BigDecimal.ZERO; + + return total.divide(new BigDecimal(nombreMembres), 2, RoundingMode.HALF_UP); + } + + private BigDecimal calculerKPINombreEvenements( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long count = evenementRepository.countEvenements(organisationId, dateDebut, dateFin); + return new BigDecimal(count); + } + + private BigDecimal calculerKPITauxParticipation( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + // Calcul basé sur les participations aux événements + Long totalParticipations = + evenementRepository.countTotalParticipations(organisationId, dateDebut, dateFin); + Long nombreEvenements = evenementRepository.countEvenements(organisationId, dateDebut, dateFin); + Long nombreMembres = membreRepository.countMembresActifs(organisationId, dateDebut, dateFin); + + if (nombreEvenements == 0 || nombreMembres == 0) return BigDecimal.ZERO; + + BigDecimal participationsAttendues = + new BigDecimal(nombreEvenements).multiply(new BigDecimal(nombreMembres)); + BigDecimal tauxParticipation = + new BigDecimal(totalParticipations) + .divide(participationsAttendues, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + + return tauxParticipation; + } + + private BigDecimal calculerKPIMoyenneParticipants( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Double moyenne = + evenementRepository.calculerMoyenneParticipants(organisationId, dateDebut, dateFin); + return moyenne != null + ? new BigDecimal(moyenne).setScale(1, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + } + + private BigDecimal calculerKPINombreDemandesAide( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long count = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin); + return new BigDecimal(count); + } + + private BigDecimal calculerKPIMontantAides( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + BigDecimal total = + demandeAideRepository.sumMontantsAccordes(organisationId, dateDebut, dateFin); + return total != null ? total : BigDecimal.ZERO; + } + + private BigDecimal calculerKPITauxApprobationAides( + UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) { + Long totalDemandes = demandeAideRepository.countDemandes(organisationId, dateDebut, dateFin); + Long demandesApprouvees = + demandeAideRepository.countDemandesApprouvees(organisationId, dateDebut, dateFin); + + if (totalDemandes == 0) return BigDecimal.ZERO; + + return new BigDecimal(demandesApprouvees) + .divide(new BigDecimal(totalDemandes), 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + } + + // === MÉTHODES UTILITAIRES === + + private BigDecimal calculerTauxCroissance( + BigDecimal valeurActuelle, BigDecimal valeurPrecedente) { + if (valeurPrecedente.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO; + + return valeurActuelle + .subtract(valeurPrecedente) + .divide(valeurPrecedente, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + } + + private BigDecimal calculerPourcentageEvolution( + BigDecimal valeurActuelle, BigDecimal valeurPrecedente) { + if (valeurPrecedente == null || valeurPrecedente.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + + return valeurActuelle + .subtract(valeurPrecedente) + .divide(valeurPrecedente, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + } + + private BigDecimal calculerScoreMembres(Map kpis) { + // Score basé sur la croissance et l'activité des membres + BigDecimal tauxCroissance = kpis.get(TypeMetrique.TAUX_CROISSANCE_MEMBRES); + BigDecimal nombreActifs = kpis.get(TypeMetrique.NOMBRE_MEMBRES_ACTIFS); + BigDecimal nombreInactifs = kpis.get(TypeMetrique.NOMBRE_MEMBRES_INACTIFS); + + // Calcul du score (logique simplifiée) + BigDecimal scoreActivite = + nombreActifs + .divide(nombreActifs.add(nombreInactifs), 2, RoundingMode.HALF_UP) + .multiply(new BigDecimal("50")); + BigDecimal scoreCroissance = tauxCroissance.min(new BigDecimal("50")); // Plafonné à 50 + + return scoreActivite.add(scoreCroissance); + } + + private BigDecimal calculerScoreFinancier(Map kpis) { + // Score basé sur le recouvrement et les montants + BigDecimal tauxRecouvrement = kpis.get(TypeMetrique.TAUX_RECOUVREMENT_COTISATIONS); + return tauxRecouvrement; // Score direct basé sur le taux de recouvrement + } + + private BigDecimal calculerScoreEvenements(Map kpis) { + // Score basé sur la participation aux événements + BigDecimal tauxParticipation = kpis.get(TypeMetrique.TAUX_PARTICIPATION_EVENEMENTS); + return tauxParticipation; // Score direct basé sur le taux de participation + } + + private BigDecimal calculerScoreSolidarite(Map kpis) { + // Score basé sur l'efficacité du système de solidarité + BigDecimal tauxApprobation = kpis.get(TypeMetrique.TAUX_APPROBATION_AIDES); + return tauxApprobation; // Score direct basé sur le taux d'approbation + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/KeycloakService.java b/src/main/java/dev/lions/unionflow/server/service/KeycloakService.java new file mode 100644 index 0000000..89bc5ae --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/KeycloakService.java @@ -0,0 +1,311 @@ +package dev.lions.unionflow.server.service; + +import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.util.Set; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.jboss.logging.Logger; + +/** + * Service pour l'intégration avec Keycloak + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@ApplicationScoped +public class KeycloakService { + + private static final Logger LOG = Logger.getLogger(KeycloakService.class); + + @Inject SecurityIdentity securityIdentity; + + @Inject JsonWebToken jwt; + + /** + * Vérifie si l'utilisateur actuel est authentifié + * + * @return true si l'utilisateur est authentifié + */ + public boolean isAuthenticated() { + return securityIdentity != null && !securityIdentity.isAnonymous(); + } + + /** + * Obtient l'ID de l'utilisateur actuel depuis Keycloak + * + * @return l'ID de l'utilisateur ou null si non authentifié + */ + public String getCurrentUserId() { + if (!isAuthenticated()) { + return null; + } + + try { + return jwt.getSubject(); + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération de l'ID utilisateur: %s", e.getMessage()); + return null; + } + } + + /** + * Obtient l'email de l'utilisateur actuel + * + * @return l'email de l'utilisateur ou null si non authentifié + */ + public String getCurrentUserEmail() { + if (!isAuthenticated()) { + return null; + } + + try { + return jwt.getClaim("email"); + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération de l'email utilisateur: %s", e.getMessage()); + return securityIdentity.getPrincipal().getName(); + } + } + + /** + * Obtient le nom complet de l'utilisateur actuel + * + * @return le nom complet ou null si non disponible + */ + public String getCurrentUserFullName() { + if (!isAuthenticated()) { + return null; + } + + try { + String firstName = jwt.getClaim("given_name"); + String lastName = jwt.getClaim("family_name"); + + if (firstName != null && lastName != null) { + return firstName + " " + lastName; + } else if (firstName != null) { + return firstName; + } else if (lastName != null) { + return lastName; + } + + // Fallback sur le nom d'utilisateur + return jwt.getClaim("preferred_username"); + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération du nom utilisateur: %s", e.getMessage()); + return null; + } + } + + /** + * Obtient tous les rôles de l'utilisateur actuel + * + * @return les rôles de l'utilisateur + */ + public Set getCurrentUserRoles() { + if (!isAuthenticated()) { + return Set.of(); + } + + return securityIdentity.getRoles(); + } + + /** + * Vérifie si l'utilisateur actuel a un rôle spécifique + * + * @param role le rôle à vérifier + * @return true si l'utilisateur a le rôle + */ + public boolean hasRole(String role) { + if (!isAuthenticated()) { + return false; + } + + return securityIdentity.hasRole(role); + } + + /** + * Vérifie si l'utilisateur actuel a au moins un des rôles spécifiés + * + * @param roles les rôles à vérifier + * @return true si l'utilisateur a au moins un des rôles + */ + public boolean hasAnyRole(String... roles) { + if (!isAuthenticated()) { + return false; + } + + for (String role : roles) { + if (securityIdentity.hasRole(role)) { + return true; + } + } + return false; + } + + /** + * Vérifie si l'utilisateur actuel a tous les rôles spécifiés + * + * @param roles les rôles à vérifier + * @return true si l'utilisateur a tous les rôles + */ + public boolean hasAllRoles(String... roles) { + if (!isAuthenticated()) { + return false; + } + + for (String role : roles) { + if (!securityIdentity.hasRole(role)) { + return false; + } + } + return true; + } + + /** + * Obtient une claim spécifique du JWT + * + * @param claimName le nom de la claim + * @return la valeur de la claim ou null si non trouvée + */ + public T getClaim(String claimName) { + if (!isAuthenticated()) { + return null; + } + + try { + return jwt.getClaim(claimName); + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération de la claim %s: %s", claimName, e.getMessage()); + return null; + } + } + + /** + * Obtient toutes les claims du JWT + * + * @return toutes les claims ou une map vide si non authentifié + */ + public Set getAllClaimNames() { + if (!isAuthenticated()) { + return Set.of(); + } + + try { + return jwt.getClaimNames(); + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération des claims: %s", e.getMessage()); + return Set.of(); + } + } + + /** + * Obtient les informations utilisateur pour les logs + * + * @return informations utilisateur formatées + */ + public String getUserInfoForLogging() { + if (!isAuthenticated()) { + return "Utilisateur non authentifié"; + } + + String email = getCurrentUserEmail(); + String fullName = getCurrentUserFullName(); + Set roles = getCurrentUserRoles(); + + return String.format( + "Utilisateur: %s (%s), Rôles: %s", + fullName != null ? fullName : "N/A", email != null ? email : "N/A", roles); + } + + /** + * Vérifie si l'utilisateur actuel est un administrateur + * + * @return true si l'utilisateur est administrateur + */ + public boolean isAdmin() { + return hasRole("ADMIN") || hasRole("admin"); + } + + /** + * Vérifie si l'utilisateur actuel peut gérer les membres + * + * @return true si l'utilisateur peut gérer les membres + */ + public boolean canManageMembers() { + return hasAnyRole( + "ADMIN", + "GESTIONNAIRE_MEMBRE", + "PRESIDENT", + "SECRETAIRE", + "admin", + "gestionnaire_membre", + "president", + "secretaire"); + } + + /** + * Vérifie si l'utilisateur actuel peut gérer les finances + * + * @return true si l'utilisateur peut gérer les finances + */ + public boolean canManageFinances() { + return hasAnyRole("ADMIN", "TRESORIER", "PRESIDENT", "admin", "tresorier", "president"); + } + + /** + * Vérifie si l'utilisateur actuel peut gérer les événements + * + * @return true si l'utilisateur peut gérer les événements + */ + public boolean canManageEvents() { + return hasAnyRole( + "ADMIN", + "ORGANISATEUR_EVENEMENT", + "PRESIDENT", + "SECRETAIRE", + "admin", + "organisateur_evenement", + "president", + "secretaire"); + } + + /** + * Vérifie si l'utilisateur actuel peut gérer les organisations + * + * @return true si l'utilisateur peut gérer les organisations + */ + public boolean canManageOrganizations() { + return hasAnyRole("ADMIN", "PRESIDENT", "admin", "president"); + } + + /** Log les informations de sécurité pour debug */ + public void logSecurityInfo() { + if (LOG.isDebugEnabled()) { + LOG.debugf("Informations de sécurité: %s", getUserInfoForLogging()); + } + } + + /** + * Obtient le token d'accès brut + * + * @return le token JWT brut ou null si non disponible + */ + public String getRawAccessToken() { + if (!isAuthenticated()) { + return null; + } + + try { + if (jwt instanceof OidcJwtCallerPrincipal) { + return ((OidcJwtCallerPrincipal) jwt).getRawToken(); + } + return jwt.getRawToken(); + } catch (Exception e) { + LOG.warnf("Erreur lors de la récupération du token brut: %s", e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/MatchingService.java b/src/main/java/dev/lions/unionflow/server/service/MatchingService.java new file mode 100644 index 0000000..d66eafc --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/MatchingService.java @@ -0,0 +1,428 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO; +import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +/** + * Service intelligent de matching entre demandes et propositions d'aide + * + *

Ce service utilise des algorithmes avancés pour faire correspondre les demandes d'aide avec + * les propositions les plus appropriées. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-16 + */ +@ApplicationScoped +public class MatchingService { + + private static final Logger LOG = Logger.getLogger(MatchingService.class); + + @Inject PropositionAideService propositionAideService; + + @Inject DemandeAideService demandeAideService; + + @ConfigProperty(name = "unionflow.matching.score-minimum", defaultValue = "30.0") + double scoreMinimumMatching; + + @ConfigProperty(name = "unionflow.matching.max-resultats", defaultValue = "10") + int maxResultatsMatching; + + @ConfigProperty(name = "unionflow.matching.boost-geographique", defaultValue = "10.0") + double boostGeographique; + + @ConfigProperty(name = "unionflow.matching.boost-experience", defaultValue = "5.0") + double boostExperience; + + // === MATCHING DEMANDES -> PROPOSITIONS === + + /** + * Trouve les propositions compatibles avec une demande d'aide + * + * @param demande La demande d'aide + * @return Liste des propositions compatibles triées par score + */ + public List trouverPropositionsCompatibles(DemandeAideDTO demande) { + LOG.infof("Recherche de propositions compatibles pour la demande: %s", demande.getId()); + + long startTime = System.currentTimeMillis(); + + try { + // 1. Recherche de base par type d'aide + List candidats = + propositionAideService.obtenirPropositionsActives(demande.getTypeAide()); + + // 2. Si pas assez de candidats, élargir à la catégorie + if (candidats.size() < 3) { + candidats.addAll(rechercherParCategorie(demande.getTypeAide().getCategorie())); + } + + // 3. Filtrage et scoring + List resultats = + candidats.stream() + .filter(PropositionAideDTO::isActiveEtDisponible) + .filter(p -> p.peutAccepterBeneficiaires()) + .map( + proposition -> { + double score = calculerScoreCompatibilite(demande, proposition); + return new ResultatMatching(proposition, score); + }) + .filter(resultat -> resultat.score >= scoreMinimumMatching) + .sorted((r1, r2) -> Double.compare(r2.score, r1.score)) + .limit(maxResultatsMatching) + .collect(Collectors.toList()); + + // 4. Extraction des propositions + List propositionsCompatibles = + resultats.stream() + .map( + resultat -> { + // Stocker le score dans les données personnalisées + if (resultat.proposition.getDonneesPersonnalisees() == null) { + resultat.proposition.setDonneesPersonnalisees(new HashMap<>()); + } + resultat + .proposition + .getDonneesPersonnalisees() + .put("scoreMatching", resultat.score); + return resultat.proposition; + }) + .collect(Collectors.toList()); + + long duration = System.currentTimeMillis() - startTime; + LOG.infof( + "Matching terminé en %d ms. Trouvé %d propositions compatibles", + duration, propositionsCompatibles.size()); + + return propositionsCompatibles; + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du matching pour la demande: %s", demande.getId()); + return new ArrayList<>(); + } + } + + /** + * Trouve les demandes compatibles avec une proposition d'aide + * + * @param proposition La proposition d'aide + * @return Liste des demandes compatibles triées par score + */ + public List trouverDemandesCompatibles(PropositionAideDTO proposition) { + LOG.infof("Recherche de demandes compatibles pour la proposition: %s", proposition.getId()); + + try { + // Recherche des demandes actives du même type + Map filtres = + Map.of( + "typeAide", proposition.getTypeAide(), + "statut", + List.of( + dev.lions.unionflow.server.api.enums.solidarite.StatutAide.SOUMISE, + dev.lions.unionflow.server.api.enums.solidarite.StatutAide.EN_ATTENTE, + dev.lions.unionflow.server.api.enums.solidarite.StatutAide + .EN_COURS_EVALUATION, + dev.lions.unionflow.server.api.enums.solidarite.StatutAide.APPROUVEE)); + + List candidats = demandeAideService.rechercherAvecFiltres(filtres); + + // Scoring et tri + return candidats.stream() + .map( + demande -> { + double score = calculerScoreCompatibilite(demande, proposition); + // Stocker le score temporairement + if (demande.getDonneesPersonnalisees() == null) { + demande.setDonneesPersonnalisees(new HashMap<>()); + } + demande.getDonneesPersonnalisees().put("scoreMatching", score); + return demande; + }) + .filter( + demande -> + (Double) demande.getDonneesPersonnalisees().get("scoreMatching") + >= scoreMinimumMatching) + .sorted( + (d1, d2) -> { + Double score1 = (Double) d1.getDonneesPersonnalisees().get("scoreMatching"); + Double score2 = (Double) d2.getDonneesPersonnalisees().get("scoreMatching"); + return Double.compare(score2, score1); + }) + .limit(maxResultatsMatching) + .collect(Collectors.toList()); + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors du matching pour la proposition: %s", proposition.getId()); + return new ArrayList<>(); + } + } + + // === MATCHING SPÉCIALISÉ === + + /** + * Recherche spécialisée de proposants financiers pour une demande approuvée + * + * @param demande La demande d'aide financière approuvée + * @return Liste des proposants financiers compatibles + */ + public List rechercherProposantsFinanciers(DemandeAideDTO demande) { + LOG.infof("Recherche de proposants financiers pour la demande: %s", demande.getId()); + + if (!demande.getTypeAide().isFinancier()) { + LOG.warnf("La demande %s n'est pas de type financier", demande.getId()); + return new ArrayList<>(); + } + + // Filtres spécifiques pour les aides financières + Map filtres = + Map.of( + "typeAide", + demande.getTypeAide(), + "estDisponible", + true, + "montantMaximum", + demande.getMontantApprouve() != null + ? demande.getMontantApprouve() + : demande.getMontantDemande()); + + List propositions = propositionAideService.rechercherAvecFiltres(filtres); + + // Scoring spécialisé pour les aides financières + return propositions.stream() + .map( + proposition -> { + double score = calculerScoreFinancier(demande, proposition); + if (proposition.getDonneesPersonnalisees() == null) { + proposition.setDonneesPersonnalisees(new HashMap<>()); + } + proposition.getDonneesPersonnalisees().put("scoreFinancier", score); + return proposition; + }) + .filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreFinancier") >= 40.0) + .sorted( + (p1, p2) -> { + Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreFinancier"); + Double score2 = (Double) p2.getDonneesPersonnalisees().get("scoreFinancier"); + return Double.compare(score2, score1); + }) + .limit(5) // Limiter à 5 pour les aides financières + .collect(Collectors.toList()); + } + + /** + * Matching d'urgence pour les demandes critiques + * + * @param demande La demande d'aide urgente + * @return Liste des propositions d'urgence + */ + public List matchingUrgence(DemandeAideDTO demande) { + LOG.infof("Matching d'urgence pour la demande: %s", demande.getId()); + + // Recherche élargie pour les urgences + List candidats = new ArrayList<>(); + + // 1. Même type d'aide + candidats.addAll(propositionAideService.obtenirPropositionsActives(demande.getTypeAide())); + + // 2. Types d'aide de la même catégorie + candidats.addAll(rechercherParCategorie(demande.getTypeAide().getCategorie())); + + // 3. Propositions généralistes (type AUTRE) + candidats.addAll(propositionAideService.obtenirPropositionsActives(TypeAide.AUTRE)); + + // Scoring avec bonus d'urgence + return candidats.stream() + .distinct() + .filter(PropositionAideDTO::isActiveEtDisponible) + .map( + proposition -> { + double score = calculerScoreCompatibilite(demande, proposition); + // Bonus d'urgence + score += 20.0; + + if (proposition.getDonneesPersonnalisees() == null) { + proposition.setDonneesPersonnalisees(new HashMap<>()); + } + proposition.getDonneesPersonnalisees().put("scoreUrgence", score); + return proposition; + }) + .filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreUrgence") >= 25.0) + .sorted( + (p1, p2) -> { + Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreUrgence"); + Double score2 = (Double) p2.getDonneesPersonnalisees().get("scoreUrgence"); + return Double.compare(score2, score1); + }) + .limit(15) // Plus de résultats pour les urgences + .collect(Collectors.toList()); + } + + // === ALGORITHMES DE SCORING === + + /** Calcule le score de compatibilité entre une demande et une proposition */ + private double calculerScoreCompatibilite( + DemandeAideDTO demande, PropositionAideDTO proposition) { + double score = 0.0; + + // 1. Correspondance du type d'aide (40 points max) + if (demande.getTypeAide() == proposition.getTypeAide()) { + score += 40.0; + } else if (demande + .getTypeAide() + .getCategorie() + .equals(proposition.getTypeAide().getCategorie())) { + score += 25.0; + } else if (proposition.getTypeAide() == TypeAide.AUTRE) { + score += 15.0; + } + + // 2. Compatibilité financière (25 points max) + if (demande.getTypeAide().isNecessiteMontant() && proposition.getMontantMaximum() != null) { + BigDecimal montantDemande = + demande.getMontantApprouve() != null + ? demande.getMontantApprouve() + : demande.getMontantDemande(); + + if (montantDemande != null) { + if (montantDemande.compareTo(proposition.getMontantMaximum()) <= 0) { + score += 25.0; + } else { + // Pénalité proportionnelle au dépassement + double ratio = proposition.getMontantMaximum().divide(montantDemande, 4, java.math.RoundingMode.HALF_UP).doubleValue(); + score += 25.0 * ratio; + } + } + } else if (!demande.getTypeAide().isNecessiteMontant()) { + score += 25.0; // Pas de contrainte financière + } + + // 3. Expérience du proposant (15 points max) + if (proposition.getNombreBeneficiairesAides() > 0) { + score += Math.min(15.0, proposition.getNombreBeneficiairesAides() * boostExperience); + } + + // 4. Réputation (10 points max) + if (proposition.getNoteMoyenne() != null && proposition.getNombreEvaluations() >= 3) { + score += (proposition.getNoteMoyenne() - 3.0) * 3.33; // 0 à 10 points + } + + // 5. Disponibilité et capacité (10 points max) + if (proposition.peutAccepterBeneficiaires()) { + double ratioCapacite = + (double) proposition.getPlacesRestantes() / proposition.getNombreMaxBeneficiaires(); + score += 10.0 * ratioCapacite; + } + + // Bonus et malus additionnels + score += calculerBonusGeographique(demande, proposition); + score += calculerBonusTemporel(demande, proposition); + score -= calculerMalusDelai(demande, proposition); + + return Math.max(0.0, Math.min(100.0, score)); + } + + /** Calcule le score spécialisé pour les aides financières */ + private double calculerScoreFinancier(DemandeAideDTO demande, PropositionAideDTO proposition) { + double score = calculerScoreCompatibilite(demande, proposition); + + // Bonus spécifiques aux aides financières + + // 1. Historique de versements + if (proposition.getMontantTotalVerse() > 0) { + score += Math.min(10.0, proposition.getMontantTotalVerse() / 10000.0); + } + + // 2. Fiabilité (ratio versements/promesses) + if (proposition.getNombreDemandesTraitees() > 0) { + // Simulation d'un ratio de fiabilité + double ratioFiabilite = 0.9; // À calculer réellement + score += ratioFiabilite * 15.0; + } + + // 3. Rapidité de réponse + if (proposition.getDelaiReponseHeures() <= 24) { + score += 10.0; + } else if (proposition.getDelaiReponseHeures() <= 72) { + score += 5.0; + } + + return Math.max(0.0, Math.min(100.0, score)); + } + + /** Calcule le bonus géographique */ + private double calculerBonusGeographique(DemandeAideDTO demande, PropositionAideDTO proposition) { + // Simulation - dans une vraie implémentation, ceci utiliserait les données de localisation + if (demande.getLocalisation() != null && proposition.getZonesGeographiques() != null) { + // Logique de proximité géographique + return boostGeographique; + } + return 0.0; + } + + /** Calcule le bonus temporel (urgence, disponibilité) */ + private double calculerBonusTemporel(DemandeAideDTO demande, PropositionAideDTO proposition) { + double bonus = 0.0; + + // Bonus pour demande urgente + if (demande.estUrgente()) { + bonus += 5.0; + } + + // Bonus pour proposition récente + long joursDepuisCreation = + java.time.Duration.between(proposition.getDateCreation(), LocalDateTime.now()).toDays(); + if (joursDepuisCreation <= 30) { + bonus += 3.0; + } + + return bonus; + } + + /** Calcule le malus de délai */ + private double calculerMalusDelai(DemandeAideDTO demande, PropositionAideDTO proposition) { + double malus = 0.0; + + // Malus si la demande est en retard + if (demande.estDelaiDepasse()) { + malus += 5.0; + } + + // Malus si la proposition a un délai de réponse long + if (proposition.getDelaiReponseHeures() > 168) { // Plus d'une semaine + malus += 3.0; + } + + return malus; + } + + // === MÉTHODES UTILITAIRES === + + /** Recherche des propositions par catégorie */ + private List rechercherParCategorie(String categorie) { + Map filtres = Map.of("estDisponible", true); + + return propositionAideService.rechercherAvecFiltres(filtres).stream() + .filter(p -> p.getTypeAide().getCategorie().equals(categorie)) + .collect(Collectors.toList()); + } + + /** Classe interne pour stocker les résultats de matching */ + private static class ResultatMatching { + final PropositionAideDTO proposition; + final double score; + + ResultatMatching(PropositionAideDTO proposition, double score) { + this.proposition = proposition; + this.score = score; + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/MembreImportExportService.java b/src/main/java/dev/lions/unionflow/server/service/MembreImportExportService.java new file mode 100644 index 0000000..057fb5a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/MembreImportExportService.java @@ -0,0 +1,842 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.membre.MembreDTO; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.CSVRecord; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.jboss.logging.Logger; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.*; + +/** + * Service pour l'import et l'export de membres depuis/vers Excel et CSV + */ +@ApplicationScoped +public class MembreImportExportService { + + private static final Logger LOG = Logger.getLogger(MembreImportExportService.class); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy"); + + @Inject + MembreRepository membreRepository; + + @Inject + OrganisationRepository organisationRepository; + + @Inject + MembreService membreService; + + /** + * Importe des membres depuis un fichier Excel ou CSV + */ + @Transactional + public ResultatImport importerMembres( + InputStream fileInputStream, + String fileName, + UUID organisationId, + String typeMembreDefaut, + boolean mettreAJourExistants, + boolean ignorerErreurs) { + + LOG.infof("Import de membres depuis le fichier: %s", fileName); + + ResultatImport resultat = new ResultatImport(); + resultat.erreurs = new ArrayList<>(); + resultat.membresImportes = new ArrayList<>(); + + try { + if (fileName.toLowerCase().endsWith(".csv")) { + return importerDepuisCSV(fileInputStream, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs); + } else if (fileName.toLowerCase().endsWith(".xlsx") || fileName.toLowerCase().endsWith(".xls")) { + return importerDepuisExcel(fileInputStream, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs); + } else { + throw new IllegalArgumentException("Format de fichier non supporté. Formats acceptés: .xlsx, .xls, .csv"); + } + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'import"); + resultat.erreurs.add("Erreur générale: " + e.getMessage()); + return resultat; + } + } + + /** + * Importe depuis un fichier Excel + */ + private ResultatImport importerDepuisExcel( + InputStream fileInputStream, + UUID organisationId, + String typeMembreDefaut, + boolean mettreAJourExistants, + boolean ignorerErreurs) throws IOException { + + ResultatImport resultat = new ResultatImport(); + resultat.erreurs = new ArrayList<>(); + resultat.membresImportes = new ArrayList<>(); + int ligneNum = 0; + + try (Workbook workbook = new XSSFWorkbook(fileInputStream)) { + Sheet sheet = workbook.getSheetAt(0); + Row headerRow = sheet.getRow(0); + + if (headerRow == null) { + throw new IllegalArgumentException("Le fichier Excel est vide ou n'a pas d'en-têtes"); + } + + // Mapper les colonnes + Map colonnes = mapperColonnes(headerRow); + + // Vérifier les colonnes obligatoires + if (!colonnes.containsKey("nom") || !colonnes.containsKey("prenom") || + !colonnes.containsKey("email") || !colonnes.containsKey("telephone")) { + throw new IllegalArgumentException("Colonnes obligatoires manquantes: nom, prenom, email, telephone"); + } + + // Lire les données + for (int i = 1; i <= sheet.getLastRowNum(); i++) { + ligneNum = i + 1; + Row row = sheet.getRow(i); + + if (row == null) { + continue; + } + + try { + Membre membre = lireLigneExcel(row, colonnes, organisationId, typeMembreDefaut); + + // Vérifier si le membre existe déjà + Optional membreExistant = membreRepository.findByEmail(membre.getEmail()); + + if (membreExistant.isPresent()) { + if (mettreAJourExistants) { + Membre existant = membreExistant.get(); + existant.setNom(membre.getNom()); + existant.setPrenom(membre.getPrenom()); + existant.setTelephone(membre.getTelephone()); + existant.setDateNaissance(membre.getDateNaissance()); + if (membre.getOrganisation() != null) { + existant.setOrganisation(membre.getOrganisation()); + } + membreRepository.persist(existant); + resultat.membresImportes.add(membreService.convertToDTO(existant)); + resultat.lignesTraitees++; + } else { + resultat.erreurs.add(String.format("Ligne %d: Membre avec email %s existe déjà", ligneNum, membre.getEmail())); + if (!ignorerErreurs) { + throw new IllegalArgumentException("Membre existant trouvé et mise à jour désactivée"); + } + } + } else { + membre = membreService.creerMembre(membre); + resultat.membresImportes.add(membreService.convertToDTO(membre)); + resultat.lignesTraitees++; + } + } catch (Exception e) { + String erreur = String.format("Ligne %d: %s", ligneNum, e.getMessage()); + resultat.erreurs.add(erreur); + resultat.lignesErreur++; + + if (!ignorerErreurs) { + throw new RuntimeException(erreur, e); + } + } + } + + resultat.totalLignes = sheet.getLastRowNum(); + } + + LOG.infof("Import terminé: %d lignes traitées, %d erreurs", resultat.lignesTraitees, resultat.lignesErreur); + return resultat; + } + + /** + * Importe depuis un fichier CSV + */ + private ResultatImport importerDepuisCSV( + InputStream fileInputStream, + UUID organisationId, + String typeMembreDefaut, + boolean mettreAJourExistants, + boolean ignorerErreurs) throws IOException { + + ResultatImport resultat = new ResultatImport(); + resultat.erreurs = new ArrayList<>(); + resultat.membresImportes = new ArrayList<>(); + + try (InputStreamReader reader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8)) { + Iterable records = CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true).build().parse(reader); + + int ligneNum = 0; + for (CSVRecord record : records) { + ligneNum++; + + try { + Membre membre = lireLigneCSV(record, organisationId, typeMembreDefaut); + + // Vérifier si le membre existe déjà + Optional membreExistant = membreRepository.findByEmail(membre.getEmail()); + + if (membreExistant.isPresent()) { + if (mettreAJourExistants) { + Membre existant = membreExistant.get(); + existant.setNom(membre.getNom()); + existant.setPrenom(membre.getPrenom()); + existant.setTelephone(membre.getTelephone()); + existant.setDateNaissance(membre.getDateNaissance()); + if (membre.getOrganisation() != null) { + existant.setOrganisation(membre.getOrganisation()); + } + membreRepository.persist(existant); + resultat.membresImportes.add(membreService.convertToDTO(existant)); + resultat.lignesTraitees++; + } else { + resultat.erreurs.add(String.format("Ligne %d: Membre avec email %s existe déjà", ligneNum, membre.getEmail())); + if (!ignorerErreurs) { + throw new IllegalArgumentException("Membre existant trouvé et mise à jour désactivée"); + } + } + } else { + membre = membreService.creerMembre(membre); + resultat.membresImportes.add(membreService.convertToDTO(membre)); + resultat.lignesTraitees++; + } + } catch (Exception e) { + String erreur = String.format("Ligne %d: %s", ligneNum, e.getMessage()); + resultat.erreurs.add(erreur); + resultat.lignesErreur++; + + if (!ignorerErreurs) { + throw new RuntimeException(erreur, e); + } + } + } + + resultat.totalLignes = ligneNum; + } + + LOG.infof("Import CSV terminé: %d lignes traitées, %d erreurs", resultat.lignesTraitees, resultat.lignesErreur); + return resultat; + } + + /** + * Lit une ligne Excel et crée un membre + */ + private Membre lireLigneExcel(Row row, Map colonnes, UUID organisationId, String typeMembreDefaut) { + Membre membre = new Membre(); + + // Colonnes obligatoires + String nom = getCellValueAsString(row, colonnes.get("nom")); + String prenom = getCellValueAsString(row, colonnes.get("prenom")); + String email = getCellValueAsString(row, colonnes.get("email")); + String telephone = getCellValueAsString(row, colonnes.get("telephone")); + + if (nom == null || nom.trim().isEmpty()) { + throw new IllegalArgumentException("Le nom est obligatoire"); + } + if (prenom == null || prenom.trim().isEmpty()) { + throw new IllegalArgumentException("Le prénom est obligatoire"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("L'email est obligatoire"); + } + if (telephone == null || telephone.trim().isEmpty()) { + throw new IllegalArgumentException("Le téléphone est obligatoire"); + } + + membre.setNom(nom.trim()); + membre.setPrenom(prenom.trim()); + membre.setEmail(email.trim().toLowerCase()); + membre.setTelephone(telephone.trim()); + + // Colonnes optionnelles + if (colonnes.containsKey("date_naissance")) { + LocalDate dateNaissance = getCellValueAsDate(row, colonnes.get("date_naissance")); + if (dateNaissance != null) { + membre.setDateNaissance(dateNaissance); + } + } + if (membre.getDateNaissance() == null) { + membre.setDateNaissance(LocalDate.now().minusYears(18)); + } + + if (colonnes.containsKey("date_adhesion")) { + LocalDate dateAdhesion = getCellValueAsDate(row, colonnes.get("date_adhesion")); + if (dateAdhesion != null) { + membre.setDateAdhesion(dateAdhesion); + } + } + if (membre.getDateAdhesion() == null) { + membre.setDateAdhesion(LocalDate.now()); + } + + // Organisation + if (organisationId != null) { + Optional org = organisationRepository.findByIdOptional(organisationId); + if (org.isPresent()) { + membre.setOrganisation(org.get()); + } + } + + // Statut par défaut + membre.setActif(typeMembreDefaut == null || typeMembreDefaut.isEmpty() || "ACTIF".equals(typeMembreDefaut)); + + return membre; + } + + /** + * Lit une ligne CSV et crée un membre + */ + private Membre lireLigneCSV(CSVRecord record, UUID organisationId, String typeMembreDefaut) { + Membre membre = new Membre(); + + // Colonnes obligatoires + String nom = record.get("nom"); + String prenom = record.get("prenom"); + String email = record.get("email"); + String telephone = record.get("telephone"); + + if (nom == null || nom.trim().isEmpty()) { + throw new IllegalArgumentException("Le nom est obligatoire"); + } + if (prenom == null || prenom.trim().isEmpty()) { + throw new IllegalArgumentException("Le prénom est obligatoire"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("L'email est obligatoire"); + } + if (telephone == null || telephone.trim().isEmpty()) { + throw new IllegalArgumentException("Le téléphone est obligatoire"); + } + + membre.setNom(nom.trim()); + membre.setPrenom(prenom.trim()); + membre.setEmail(email.trim().toLowerCase()); + membre.setTelephone(telephone.trim()); + + // Colonnes optionnelles + try { + String dateNaissanceStr = record.get("date_naissance"); + if (dateNaissanceStr != null && !dateNaissanceStr.trim().isEmpty()) { + membre.setDateNaissance(parseDate(dateNaissanceStr)); + } + } catch (Exception e) { + // Ignorer si la date est invalide + } + if (membre.getDateNaissance() == null) { + membre.setDateNaissance(LocalDate.now().minusYears(18)); + } + + try { + String dateAdhesionStr = record.get("date_adhesion"); + if (dateAdhesionStr != null && !dateAdhesionStr.trim().isEmpty()) { + membre.setDateAdhesion(parseDate(dateAdhesionStr)); + } + } catch (Exception e) { + // Ignorer si la date est invalide + } + if (membre.getDateAdhesion() == null) { + membre.setDateAdhesion(LocalDate.now()); + } + + // Organisation + if (organisationId != null) { + Optional org = organisationRepository.findByIdOptional(organisationId); + if (org.isPresent()) { + membre.setOrganisation(org.get()); + } + } + + // Statut par défaut + membre.setActif(typeMembreDefaut == null || typeMembreDefaut.isEmpty() || "ACTIF".equals(typeMembreDefaut)); + + return membre; + } + + /** + * Mappe les colonnes Excel + */ + private Map mapperColonnes(Row headerRow) { + Map colonnes = new HashMap<>(); + for (Cell cell : headerRow) { + String headerName = getCellValueAsString(headerRow, cell.getColumnIndex()).toLowerCase() + .replace(" ", "_") + .replace("é", "e") + .replace("è", "e") + .replace("ê", "e"); + colonnes.put(headerName, cell.getColumnIndex()); + } + return colonnes; + } + + /** + * Obtient la valeur d'une cellule comme String + */ + private String getCellValueAsString(Row row, Integer columnIndex) { + if (columnIndex == null || row == null) { + return null; + } + Cell cell = row.getCell(columnIndex); + if (cell == null) { + return null; + } + + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue().toString(); + } else { + return String.valueOf((long) cell.getNumericCellValue()); + } + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + return cell.getCellFormula(); + default: + return null; + } + } + + /** + * Obtient la valeur d'une cellule comme Date + */ + private LocalDate getCellValueAsDate(Row row, Integer columnIndex) { + if (columnIndex == null || row == null) { + return null; + } + Cell cell = row.getCell(columnIndex); + if (cell == null) { + return null; + } + + try { + if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue().toInstant() + .atZone(java.time.ZoneId.systemDefault()) + .toLocalDate(); + } else if (cell.getCellType() == CellType.STRING) { + return parseDate(cell.getStringCellValue()); + } + } catch (Exception e) { + LOG.warnf("Erreur lors de la lecture de la date: %s", e.getMessage()); + } + return null; + } + + /** + * Parse une date depuis une String + */ + private LocalDate parseDate(String dateStr) { + if (dateStr == null || dateStr.trim().isEmpty()) { + return null; + } + + dateStr = dateStr.trim(); + + // Essayer différents formats + String[] formats = { + "dd/MM/yyyy", + "yyyy-MM-dd", + "dd-MM-yyyy", + "dd.MM.yyyy" + }; + + for (String format : formats) { + try { + return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(format)); + } catch (DateTimeParseException e) { + // Continuer avec le format suivant + } + } + + throw new IllegalArgumentException("Format de date non reconnu: " + dateStr); + } + + /** + * Exporte des membres vers Excel + */ + public byte[] exporterVersExcel(List membres, List colonnesExport, boolean inclureHeaders, boolean formaterDates, boolean inclureStatistiques, String motDePasse) throws IOException { + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("Membres"); + + int rowNum = 0; + + // En-têtes + if (inclureHeaders) { + Row headerRow = sheet.createRow(rowNum++); + int colNum = 0; + + if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) { + headerRow.createCell(colNum++).setCellValue("Nom"); + headerRow.createCell(colNum++).setCellValue("Prénom"); + headerRow.createCell(colNum++).setCellValue("Date de naissance"); + } + if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) { + headerRow.createCell(colNum++).setCellValue("Email"); + headerRow.createCell(colNum++).setCellValue("Téléphone"); + } + if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) { + headerRow.createCell(colNum++).setCellValue("Date adhésion"); + headerRow.createCell(colNum++).setCellValue("Statut"); + } + if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) { + headerRow.createCell(colNum++).setCellValue("Organisation"); + } + } + + // Données + for (MembreDTO membre : membres) { + Row row = sheet.createRow(rowNum++); + int colNum = 0; + + if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) { + row.createCell(colNum++).setCellValue(membre.getNom() != null ? membre.getNom() : ""); + row.createCell(colNum++).setCellValue(membre.getPrenom() != null ? membre.getPrenom() : ""); + if (membre.getDateNaissance() != null) { + Cell dateCell = row.createCell(colNum++); + if (formaterDates) { + dateCell.setCellValue(membre.getDateNaissance().format(DATE_FORMATTER)); + } else { + dateCell.setCellValue(membre.getDateNaissance().toString()); + } + } else { + row.createCell(colNum++).setCellValue(""); + } + } + if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) { + row.createCell(colNum++).setCellValue(membre.getEmail() != null ? membre.getEmail() : ""); + row.createCell(colNum++).setCellValue(membre.getTelephone() != null ? membre.getTelephone() : ""); + } + if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) { + if (membre.getDateAdhesion() != null) { + Cell dateCell = row.createCell(colNum++); + if (formaterDates) { + dateCell.setCellValue(membre.getDateAdhesion().format(DATE_FORMATTER)); + } else { + dateCell.setCellValue(membre.getDateAdhesion().toString()); + } + } else { + row.createCell(colNum++).setCellValue(""); + } + row.createCell(colNum++).setCellValue(membre.getStatut() != null ? membre.getStatut().toString() : ""); + } + if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) { + row.createCell(colNum++).setCellValue(membre.getAssociationNom() != null ? membre.getAssociationNom() : ""); + } + } + + // Auto-size columns + for (int i = 0; i < 10; i++) { + sheet.autoSizeColumn(i); + } + + // Ajouter un onglet statistiques si demandé + if (inclureStatistiques && !membres.isEmpty()) { + Sheet statsSheet = workbook.createSheet("Statistiques"); + creerOngletStatistiques(statsSheet, membres); + } + + // Écrire dans un ByteArrayOutputStream + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + workbook.write(outputStream); + byte[] excelData = outputStream.toByteArray(); + + // Chiffrer le fichier si un mot de passe est fourni + if (motDePasse != null && !motDePasse.trim().isEmpty()) { + return chiffrerExcel(excelData, motDePasse); + } + + return excelData; + } + } + } + + /** + * Crée un onglet statistiques dans le classeur Excel + */ + private void creerOngletStatistiques(Sheet sheet, List membres) { + int rowNum = 0; + + // Titre + Row titleRow = sheet.createRow(rowNum++); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellValue("Statistiques des Membres"); + CellStyle titleStyle = sheet.getWorkbook().createCellStyle(); + Font titleFont = sheet.getWorkbook().createFont(); + titleFont.setBold(true); + titleFont.setFontHeightInPoints((short) 14); + titleStyle.setFont(titleFont); + titleCell.setCellStyle(titleStyle); + + rowNum++; // Ligne vide + + // Statistiques générales + Row headerRow = sheet.createRow(rowNum++); + headerRow.createCell(0).setCellValue("Indicateur"); + headerRow.createCell(1).setCellValue("Valeur"); + + // Style pour les en-têtes + CellStyle headerStyle = sheet.getWorkbook().createCellStyle(); + Font headerFont = sheet.getWorkbook().createFont(); + headerFont.setBold(true); + headerStyle.setFont(headerFont); + headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); + headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + headerRow.getCell(0).setCellStyle(headerStyle); + headerRow.getCell(1).setCellStyle(headerStyle); + + // Calcul des statistiques + long totalMembres = membres.size(); + long membresActifs = membres.stream().filter(m -> "ACTIF".equals(m.getStatut())).count(); + long membresInactifs = membres.stream().filter(m -> "INACTIF".equals(m.getStatut())).count(); + long membresSuspendus = membres.stream().filter(m -> "SUSPENDU".equals(m.getStatut())).count(); + + // Organisations distinctes + long organisationsDistinctes = membres.stream() + .filter(m -> m.getAssociationNom() != null) + .map(MembreDTO::getAssociationNom) + .distinct() + .count(); + + // Statistiques par type (si disponible dans le DTO) + // Note: Le type de membre peut ne pas être disponible dans MembreDTO + // Pour l'instant, on utilise le statut comme indicateur + long typeActif = membresActifs; + long typeAssocie = 0; + long typeBienfaiteur = 0; + long typeHonoraire = 0; + + // Ajout des statistiques + int currentRow = rowNum; + sheet.createRow(currentRow++).createCell(0).setCellValue("Total Membres"); + sheet.getRow(currentRow - 1).createCell(1).setCellValue(totalMembres); + + sheet.createRow(currentRow++).createCell(0).setCellValue("Membres Actifs"); + sheet.getRow(currentRow - 1).createCell(1).setCellValue(membresActifs); + + sheet.createRow(currentRow++).createCell(0).setCellValue("Membres Inactifs"); + sheet.getRow(currentRow - 1).createCell(1).setCellValue(membresInactifs); + + sheet.createRow(currentRow++).createCell(0).setCellValue("Membres Suspendus"); + sheet.getRow(currentRow - 1).createCell(1).setCellValue(membresSuspendus); + + sheet.createRow(currentRow++).createCell(0).setCellValue("Organisations Distinctes"); + sheet.getRow(currentRow - 1).createCell(1).setCellValue(organisationsDistinctes); + + currentRow++; // Ligne vide + + // Section par type + sheet.createRow(currentRow++).createCell(0).setCellValue("Répartition par Type"); + CellStyle sectionStyle = sheet.getWorkbook().createCellStyle(); + Font sectionFont = sheet.getWorkbook().createFont(); + sectionFont.setBold(true); + sectionStyle.setFont(sectionFont); + sheet.getRow(currentRow - 1).getCell(0).setCellStyle(sectionStyle); + + sheet.createRow(currentRow++).createCell(0).setCellValue("Type Actif"); + sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeActif); + + sheet.createRow(currentRow++).createCell(0).setCellValue("Type Associé"); + sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeAssocie); + + sheet.createRow(currentRow++).createCell(0).setCellValue("Type Bienfaiteur"); + sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeBienfaiteur); + + sheet.createRow(currentRow++).createCell(0).setCellValue("Type Honoraire"); + sheet.getRow(currentRow - 1).createCell(1).setCellValue(typeHonoraire); + + // Auto-size columns + sheet.autoSizeColumn(0); + sheet.autoSizeColumn(1); + } + + /** + * Protège un fichier Excel avec un mot de passe + * Utilise Apache POI pour protéger les feuilles et la structure du workbook + * Note: Ceci protège contre la modification, pas un chiffrement complet du fichier + */ + private byte[] chiffrerExcel(byte[] excelData, String motDePasse) throws IOException { + try { + // Pour XLSX, on protège les feuilles et la structure du workbook + // Note: POI 5.2.5 ne supporte pas le chiffrement complet XLSX (nécessite des bibliothèques externes) + // On utilise la protection par mot de passe qui empêche la modification sans le mot de passe + + try (java.io.ByteArrayInputStream inputStream = new java.io.ByteArrayInputStream(excelData); + XSSFWorkbook workbook = new XSSFWorkbook(inputStream); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + + // Protéger toutes les feuilles avec un mot de passe (empêche la modification des cellules) + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + Sheet sheet = workbook.getSheetAt(i); + sheet.protectSheet(motDePasse); + } + + // Protéger la structure du workbook (empêche l'ajout/suppression de feuilles) + org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbookProtection protection = + workbook.getCTWorkbook().getWorkbookProtection(); + if (protection == null) { + protection = workbook.getCTWorkbook().addNewWorkbookProtection(); + } + protection.setLockStructure(true); + // Le mot de passe doit être haché selon le format Excel + // Pour simplifier, on utilise le hash MD5 du mot de passe + try { + java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); + byte[] passwordHash = md.digest(motDePasse.getBytes(java.nio.charset.StandardCharsets.UTF_16LE)); + protection.setWorkbookPassword(passwordHash); + } catch (java.security.NoSuchAlgorithmException e) { + LOG.warnf("Impossible de hasher le mot de passe, protection partielle uniquement"); + } + + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la protection du fichier Excel"); + // En cas d'erreur, retourner le fichier non protégé avec un avertissement + LOG.warnf("Le fichier sera exporté sans protection en raison d'une erreur"); + return excelData; + } + } + + /** + * Exporte des membres vers CSV + */ + public byte[] exporterVersCSV(List membres, List colonnesExport, boolean inclureHeaders, boolean formaterDates) throws IOException { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + CSVPrinter printer = new CSVPrinter( + new java.io.OutputStreamWriter(outputStream, StandardCharsets.UTF_8), + CSVFormat.DEFAULT)) { + + // En-têtes + if (inclureHeaders) { + List headers = new ArrayList<>(); + if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) { + headers.add("Nom"); + headers.add("Prénom"); + headers.add("Date de naissance"); + } + if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) { + headers.add("Email"); + headers.add("Téléphone"); + } + if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) { + headers.add("Date adhésion"); + headers.add("Statut"); + } + if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) { + headers.add("Organisation"); + } + printer.printRecord(headers); + } + + // Données + for (MembreDTO membre : membres) { + List values = new ArrayList<>(); + + if (colonnesExport.contains("PERSO") || colonnesExport.isEmpty()) { + values.add(membre.getNom() != null ? membre.getNom() : ""); + values.add(membre.getPrenom() != null ? membre.getPrenom() : ""); + if (membre.getDateNaissance() != null) { + values.add(formaterDates ? membre.getDateNaissance().format(DATE_FORMATTER) : membre.getDateNaissance().toString()); + } else { + values.add(""); + } + } + if (colonnesExport.contains("CONTACT") || colonnesExport.isEmpty()) { + values.add(membre.getEmail() != null ? membre.getEmail() : ""); + values.add(membre.getTelephone() != null ? membre.getTelephone() : ""); + } + if (colonnesExport.contains("ADHESION") || colonnesExport.isEmpty()) { + if (membre.getDateAdhesion() != null) { + values.add(formaterDates ? membre.getDateAdhesion().format(DATE_FORMATTER) : membre.getDateAdhesion().toString()); + } else { + values.add(""); + } + values.add(membre.getStatut() != null ? membre.getStatut().toString() : ""); + } + if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) { + values.add(membre.getAssociationNom() != null ? membre.getAssociationNom() : ""); + } + + printer.printRecord(values); + } + + printer.flush(); + return outputStream.toByteArray(); + } + } + + /** + * Génère un modèle Excel pour l'import + */ + public byte[] genererModeleImport() throws IOException { + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("Modèle"); + + // En-têtes + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("Nom"); + headerRow.createCell(1).setCellValue("Prénom"); + headerRow.createCell(2).setCellValue("Email"); + headerRow.createCell(3).setCellValue("Téléphone"); + headerRow.createCell(4).setCellValue("Date naissance"); + headerRow.createCell(5).setCellValue("Date adhésion"); + headerRow.createCell(6).setCellValue("Adresse"); + headerRow.createCell(7).setCellValue("Profession"); + headerRow.createCell(8).setCellValue("Type membre"); + + // Exemple de ligne + Row exampleRow = sheet.createRow(1); + exampleRow.createCell(0).setCellValue("DUPONT"); + exampleRow.createCell(1).setCellValue("Jean"); + exampleRow.createCell(2).setCellValue("jean.dupont@example.com"); + exampleRow.createCell(3).setCellValue("+225 07 12 34 56 78"); + exampleRow.createCell(4).setCellValue("15/01/1990"); + exampleRow.createCell(5).setCellValue("01/01/2024"); + exampleRow.createCell(6).setCellValue("Abidjan, Cocody"); + exampleRow.createCell(7).setCellValue("Ingénieur"); + exampleRow.createCell(8).setCellValue("ACTIF"); + + // Auto-size columns + for (int i = 0; i < 9; i++) { + sheet.autoSizeColumn(i); + } + + // Écrire dans un ByteArrayOutputStream + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } + } + + /** + * Classe pour le résultat de l'import + */ + public static class ResultatImport { + public int totalLignes; + public int lignesTraitees; + public int lignesErreur; + public List erreurs; + public List membresImportes; + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/service/MembreService.java b/src/main/java/dev/lions/unionflow/server/service/MembreService.java new file mode 100644 index 0000000..375c844 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/MembreService.java @@ -0,0 +1,740 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.membre.MembreDTO; +import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria; +import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO; +import dev.lions.unionflow.server.api.enums.membre.StatutMembre; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.repository.MembreRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.TypedQuery; +import jakarta.transaction.Transactional; +import java.io.InputStream; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Period; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** Service métier pour les membres */ +@ApplicationScoped +public class MembreService { + + private static final Logger LOG = Logger.getLogger(MembreService.class); + + @Inject MembreRepository membreRepository; + + @Inject + MembreImportExportService membreImportExportService; + + @PersistenceContext + EntityManager entityManager; + + /** Crée un nouveau membre */ + @Transactional + public Membre creerMembre(Membre membre) { + LOG.infof("Création d'un nouveau membre: %s", membre.getEmail()); + + // Générer un numéro de membre unique + if (membre.getNumeroMembre() == null || membre.getNumeroMembre().isEmpty()) { + membre.setNumeroMembre(genererNumeroMembre()); + } + + // Définir la date d'adhésion si non fournie + if (membre.getDateAdhesion() == null) { + membre.setDateAdhesion(LocalDate.now()); + LOG.infof("Date d'adhésion automatiquement définie à: %s", membre.getDateAdhesion()); + } + + // Définir la date de naissance par défaut si non fournie (pour éviter @NotNull) + if (membre.getDateNaissance() == null) { + membre.setDateNaissance(LocalDate.now().minusYears(18)); // Majeur par défaut + LOG.warn("Date de naissance non fournie, définie par défaut à il y a 18 ans"); + } + + // Vérifier l'unicité de l'email + if (membreRepository.findByEmail(membre.getEmail()).isPresent()) { + throw new IllegalArgumentException("Un membre avec cet email existe déjà"); + } + + // Vérifier l'unicité du numéro de membre + if (membreRepository.findByNumeroMembre(membre.getNumeroMembre()).isPresent()) { + throw new IllegalArgumentException("Un membre avec ce numéro existe déjà"); + } + + membreRepository.persist(membre); + + // Mettre à jour le compteur de membres de l'organisation + if (membre.getOrganisation() != null) { + membre.getOrganisation().ajouterMembre(); + LOG.infof("Compteur de membres mis à jour pour l'organisation: %s", membre.getOrganisation().getNom()); + } + + LOG.infof("Membre créé avec succès: %s (ID: %s)", membre.getNomComplet(), membre.getId()); + return membre; + } + + /** Met à jour un membre existant */ + @Transactional + public Membre mettreAJourMembre(UUID id, Membre membreModifie) { + LOG.infof("Mise à jour du membre ID: %s", id); + + Membre membre = membreRepository.findById(id); + if (membre == null) { + throw new IllegalArgumentException("Membre non trouvé avec l'ID: " + id); + } + + // Vérifier l'unicité de l'email si modifié + if (!membre.getEmail().equals(membreModifie.getEmail())) { + if (membreRepository.findByEmail(membreModifie.getEmail()).isPresent()) { + throw new IllegalArgumentException("Un membre avec cet email existe déjà"); + } + } + + // Mettre à jour les champs + membre.setPrenom(membreModifie.getPrenom()); + membre.setNom(membreModifie.getNom()); + membre.setEmail(membreModifie.getEmail()); + membre.setTelephone(membreModifie.getTelephone()); + membre.setDateNaissance(membreModifie.getDateNaissance()); + membre.setActif(membreModifie.getActif()); + + LOG.infof("Membre mis à jour avec succès: %s", membre.getNomComplet()); + return membre; + } + + /** Trouve un membre par son ID */ + public Optional trouverParId(UUID id) { + return Optional.ofNullable(membreRepository.findById(id)); + } + + /** Trouve un membre par son email */ + public Optional trouverParEmail(String email) { + return membreRepository.findByEmail(email); + } + + /** Liste tous les membres actifs */ + public List listerMembresActifs() { + return membreRepository.findAllActifs(); + } + + /** Recherche des membres par nom ou prénom */ + public List rechercherMembres(String recherche) { + return membreRepository.findByNomOrPrenom(recherche); + } + + /** Désactive un membre */ + @Transactional + public void desactiverMembre(UUID id) { + LOG.infof("Désactivation du membre ID: %s", id); + + Membre membre = membreRepository.findById(id); + if (membre == null) { + throw new IllegalArgumentException("Membre non trouvé avec l'ID: " + id); + } + + membre.setActif(false); + LOG.infof("Membre désactivé: %s", membre.getNomComplet()); + } + + /** Génère un numéro de membre unique */ + private String genererNumeroMembre() { + String prefix = "UF" + LocalDate.now().getYear(); + String suffix = UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + return prefix + "-" + suffix; + } + + /** Compte le nombre total de membres actifs */ + public long compterMembresActifs() { + return membreRepository.countActifs(); + } + + /** Liste tous les membres actifs avec pagination */ + public List listerMembresActifs(Page page, Sort sort) { + return membreRepository.findAllActifs(page, sort); + } + + /** Recherche des membres avec pagination */ + public List rechercherMembres(String recherche, Page page, Sort sort) { + return membreRepository.findByNomOrPrenom(recherche, page, sort); + } + + /** Obtient les statistiques avancées des membres */ + public Map obtenirStatistiquesAvancees() { + LOG.info("Calcul des statistiques avancées des membres"); + + long totalMembres = membreRepository.count(); + long membresActifs = membreRepository.countActifs(); + long membresInactifs = totalMembres - membresActifs; + long nouveauxMembres30Jours = + membreRepository.countNouveauxMembres(LocalDate.now().minusDays(30)); + + return Map.of( + "totalMembres", totalMembres, + "membresActifs", membresActifs, + "membresInactifs", membresInactifs, + "nouveauxMembres30Jours", nouveauxMembres30Jours, + "tauxActivite", totalMembres > 0 ? (membresActifs * 100.0 / totalMembres) : 0.0, + "timestamp", LocalDateTime.now()); + } + + // ======================================== + // MÉTHODES DE CONVERSION DTO + // ======================================== + + /** Convertit une entité Membre en MembreDTO */ + public MembreDTO convertToDTO(Membre membre) { + if (membre == null) { + return null; + } + + MembreDTO dto = new MembreDTO(); + + // Conversion de l'ID UUID vers UUID (pas de conversion nécessaire maintenant) + dto.setId(membre.getId()); + + // Copie des champs de base + dto.setNumeroMembre(membre.getNumeroMembre()); + dto.setNom(membre.getNom()); + dto.setPrenom(membre.getPrenom()); + dto.setEmail(membre.getEmail()); + dto.setTelephone(membre.getTelephone()); + dto.setDateNaissance(membre.getDateNaissance()); + dto.setDateAdhesion(membre.getDateAdhesion()); + + // Conversion du statut boolean vers enum StatutMembre + // Règle métier: actif=true → ACTIF, actif=false → INACTIF + if (membre.getActif() == null || Boolean.TRUE.equals(membre.getActif())) { + dto.setStatut(StatutMembre.ACTIF); + } else { + dto.setStatut(StatutMembre.INACTIF); + } + + // Conversion de l'organisation (associationId) + // Utilisation directe de l'UUID de l'organisation + if (membre.getOrganisation() != null && membre.getOrganisation().getId() != null) { + dto.setAssociationId(membre.getOrganisation().getId()); + dto.setAssociationNom(membre.getOrganisation().getNom()); + } + + // Champs de base DTO + dto.setDateCreation(membre.getDateCreation()); + dto.setDateModification(membre.getDateModification()); + dto.setVersion(0L); // Version par défaut + + // Champs par défaut pour les champs manquants dans l'entité + dto.setMembreBureau(false); + dto.setResponsable(false); + + return dto; + } + + /** Convertit un MembreDTO en entité Membre */ + public Membre convertFromDTO(MembreDTO dto) { + if (dto == null) { + return null; + } + + Membre membre = new Membre(); + + // Copie des champs + membre.setNumeroMembre(dto.getNumeroMembre()); + membre.setNom(dto.getNom()); + membre.setPrenom(dto.getPrenom()); + membre.setEmail(dto.getEmail()); + membre.setTelephone(dto.getTelephone()); + membre.setDateNaissance(dto.getDateNaissance()); + membre.setDateAdhesion(dto.getDateAdhesion()); + + // Conversion du statut enum vers boolean + // Règle métier: ACTIF → true, autres statuts → false + membre.setActif(dto.getStatut() != null && StatutMembre.ACTIF.equals(dto.getStatut())); + + // Champs de base + if (dto.getDateCreation() != null) { + membre.setDateCreation(dto.getDateCreation()); + } + if (dto.getDateModification() != null) { + membre.setDateModification(dto.getDateModification()); + } + + return membre; + } + + /** Convertit une liste d'entités en liste de DTOs */ + public List convertToDTOList(List membres) { + return membres.stream().map(this::convertToDTO).collect(Collectors.toList()); + } + + /** Met à jour une entité Membre à partir d'un MembreDTO */ + public void updateFromDTO(Membre membre, MembreDTO dto) { + if (membre == null || dto == null) { + return; + } + + // Mise à jour des champs modifiables + membre.setPrenom(dto.getPrenom()); + membre.setNom(dto.getNom()); + membre.setEmail(dto.getEmail()); + membre.setTelephone(dto.getTelephone()); + membre.setDateNaissance(dto.getDateNaissance()); + // Conversion du statut enum vers boolean + membre.setActif(dto.getStatut() != null && StatutMembre.ACTIF.equals(dto.getStatut())); + membre.setDateModification(LocalDateTime.now()); + } + + /** Recherche avancée de membres avec filtres multiples (DEPRECATED) */ + public List rechercheAvancee( + String recherche, + Boolean actif, + LocalDate dateAdhesionMin, + LocalDate dateAdhesionMax, + Page page, + Sort sort) { + LOG.infof( + "Recherche avancée (DEPRECATED) - recherche: %s, actif: %s, dateMin: %s, dateMax: %s", + recherche, actif, dateAdhesionMin, dateAdhesionMax); + + return membreRepository.rechercheAvancee( + recherche, actif, dateAdhesionMin, dateAdhesionMax, page, sort); + } + + /** + * Nouvelle recherche avancée de membres avec critères complets Retourne des résultats paginés + * avec statistiques + * + * @param criteria Critères de recherche + * @param page Pagination + * @param sort Tri + * @return Résultats de recherche avec métadonnées + */ + public MembreSearchResultDTO searchMembresAdvanced( + MembreSearchCriteria criteria, Page page, Sort sort) { + LOG.infof("Recherche avancée de membres - critères: %s", criteria.getDescription()); + + try { + // Construction de la requête dynamique + StringBuilder queryBuilder = new StringBuilder("SELECT m FROM Membre m WHERE 1=1"); + Map parameters = new HashMap<>(); + + // Ajout des critères de recherche + addSearchCriteria(queryBuilder, parameters, criteria); + + // Requête pour compter le total + String countQuery = + queryBuilder + .toString() + .replace("SELECT m FROM Membre m", "SELECT COUNT(m) FROM Membre m"); + + // Exécution de la requête de comptage + TypedQuery countQueryTyped = entityManager.createQuery(countQuery, Long.class); + for (Map.Entry param : parameters.entrySet()) { + countQueryTyped.setParameter(param.getKey(), param.getValue()); + } + long totalElements = countQueryTyped.getSingleResult(); + + if (totalElements == 0) { + return MembreSearchResultDTO.empty(criteria, page.size, page.index); + } + + // Ajout du tri et pagination + String finalQuery = queryBuilder.toString(); + if (sort != null) { + finalQuery += " ORDER BY " + buildOrderByClause(sort); + } + + // Exécution de la requête principale + TypedQuery queryTyped = entityManager.createQuery(finalQuery, Membre.class); + for (Map.Entry param : parameters.entrySet()) { + queryTyped.setParameter(param.getKey(), param.getValue()); + } + queryTyped.setFirstResult(page.index * page.size); + queryTyped.setMaxResults(page.size); + List membres = queryTyped.getResultList(); + + // Conversion en DTOs + List membresDTO = convertToDTOList(membres); + + // Calcul des statistiques + MembreSearchResultDTO.SearchStatistics statistics = calculateSearchStatistics(membres); + + // Construction du résultat + MembreSearchResultDTO result = + MembreSearchResultDTO.builder() + .membres(membresDTO) + .totalElements(totalElements) + .totalPages((int) Math.ceil((double) totalElements / page.size)) + .currentPage(page.index) + .pageSize(page.size) + .criteria(criteria) + .statistics(statistics) + .build(); + + // Calcul des indicateurs de pagination + result.calculatePaginationFlags(); + + return result; + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la recherche avancée de membres"); + throw new RuntimeException("Erreur lors de la recherche avancée", e); + } + } + + /** Ajoute les critères de recherche à la requête */ + private void addSearchCriteria( + StringBuilder queryBuilder, Map parameters, MembreSearchCriteria criteria) { + + // Recherche générale dans nom, prénom, email + if (criteria.getQuery() != null) { + queryBuilder.append( + " AND (LOWER(m.nom) LIKE LOWER(:query) OR LOWER(m.prenom) LIKE LOWER(:query) OR" + + " LOWER(m.email) LIKE LOWER(:query))"); + parameters.put("query", "%" + criteria.getQuery() + "%"); + } + + // Recherche par nom + if (criteria.getNom() != null) { + queryBuilder.append(" AND LOWER(m.nom) LIKE LOWER(:nom)"); + parameters.put("nom", "%" + criteria.getNom() + "%"); + } + + // Recherche par prénom + if (criteria.getPrenom() != null) { + queryBuilder.append(" AND LOWER(m.prenom) LIKE LOWER(:prenom)"); + parameters.put("prenom", "%" + criteria.getPrenom() + "%"); + } + + // Recherche par email + if (criteria.getEmail() != null) { + queryBuilder.append(" AND LOWER(m.email) LIKE LOWER(:email)"); + parameters.put("email", "%" + criteria.getEmail() + "%"); + } + + // Recherche par téléphone + if (criteria.getTelephone() != null) { + queryBuilder.append(" AND m.telephone LIKE :telephone"); + parameters.put("telephone", "%" + criteria.getTelephone() + "%"); + } + + // Filtre par statut + if (criteria.getStatut() != null) { + boolean isActif = "ACTIF".equals(criteria.getStatut()); + queryBuilder.append(" AND m.actif = :actif"); + parameters.put("actif", isActif); + } else if (!Boolean.TRUE.equals(criteria.getIncludeInactifs())) { + // Par défaut, exclure les inactifs + queryBuilder.append(" AND m.actif = true"); + } + + // Filtre par dates d'adhésion + if (criteria.getDateAdhesionMin() != null) { + queryBuilder.append(" AND m.dateAdhesion >= :dateAdhesionMin"); + parameters.put("dateAdhesionMin", criteria.getDateAdhesionMin()); + } + + if (criteria.getDateAdhesionMax() != null) { + queryBuilder.append(" AND m.dateAdhesion <= :dateAdhesionMax"); + parameters.put("dateAdhesionMax", criteria.getDateAdhesionMax()); + } + + // Filtre par âge (calculé à partir de la date de naissance) + if (criteria.getAgeMin() != null) { + LocalDate maxBirthDate = LocalDate.now().minusYears(criteria.getAgeMin()); + queryBuilder.append(" AND m.dateNaissance <= :maxBirthDateForMinAge"); + parameters.put("maxBirthDateForMinAge", maxBirthDate); + } + + if (criteria.getAgeMax() != null) { + LocalDate minBirthDate = LocalDate.now().minusYears(criteria.getAgeMax() + 1).plusDays(1); + queryBuilder.append(" AND m.dateNaissance >= :minBirthDateForMaxAge"); + parameters.put("minBirthDateForMaxAge", minBirthDate); + } + + // Filtre par organisations (si implémenté dans l'entité) + if (criteria.getOrganisationIds() != null && !criteria.getOrganisationIds().isEmpty()) { + queryBuilder.append(" AND m.organisation.id IN :organisationIds"); + parameters.put("organisationIds", criteria.getOrganisationIds()); + } + + // Filtre par rôles (recherche via la relation MembreRole -> Role) + if (criteria.getRoles() != null && !criteria.getRoles().isEmpty()) { + // Utiliser EXISTS avec une sous-requête pour vérifier les rôles + queryBuilder.append(" AND EXISTS ("); + queryBuilder.append(" SELECT 1 FROM MembreRole mr WHERE mr.membre = m"); + queryBuilder.append(" AND mr.actif = true"); + queryBuilder.append(" AND mr.role.code IN :roleCodes"); + queryBuilder.append(")"); + // Convertir les noms de rôles en codes (supposant que criteria.getRoles() contient des codes) + parameters.put("roleCodes", criteria.getRoles()); + } + } + + /** Construit la clause ORDER BY à partir du Sort */ + private String buildOrderByClause(Sort sort) { + if (sort == null || sort.getColumns().isEmpty()) { + return "m.nom ASC"; + } + + return sort.getColumns().stream() + .map(column -> { + String direction = column.getDirection() == Sort.Direction.Descending ? "DESC" : "ASC"; + return "m." + column.getName() + " " + direction; + }) + .collect(Collectors.joining(", ")); + } + + /** Calcule les statistiques sur les résultats de recherche */ + private MembreSearchResultDTO.SearchStatistics calculateSearchStatistics(List membres) { + if (membres.isEmpty()) { + return MembreSearchResultDTO.SearchStatistics.builder() + .membresActifs(0) + .membresInactifs(0) + .ageMoyen(0.0) + .ageMin(0) + .ageMax(0) + .nombreOrganisations(0) + .nombreRegions(0) + .ancienneteMoyenne(0.0) + .build(); + } + + long membresActifs = + membres.stream().mapToLong(m -> Boolean.TRUE.equals(m.getActif()) ? 1 : 0).sum(); + long membresInactifs = membres.size() - membresActifs; + + // Calcul des âges + List ages = + membres.stream() + .filter(m -> m.getDateNaissance() != null) + .map(m -> Period.between(m.getDateNaissance(), LocalDate.now()).getYears()) + .collect(Collectors.toList()); + + double ageMoyen = ages.stream().mapToInt(Integer::intValue).average().orElse(0.0); + int ageMin = ages.stream().mapToInt(Integer::intValue).min().orElse(0); + int ageMax = ages.stream().mapToInt(Integer::intValue).max().orElse(0); + + // Calcul de l'ancienneté moyenne + double ancienneteMoyenne = + membres.stream() + .filter(m -> m.getDateAdhesion() != null) + .mapToDouble(m -> Period.between(m.getDateAdhesion(), LocalDate.now()).getYears()) + .average() + .orElse(0.0); + + // Nombre d'organisations (si relation disponible) + long nombreOrganisations = + membres.stream() + .filter(m -> m.getOrganisation() != null) + .map(m -> m.getOrganisation().getId()) + .distinct() + .count(); + + return MembreSearchResultDTO.SearchStatistics.builder() + .membresActifs(membresActifs) + .membresInactifs(membresInactifs) + .ageMoyen(ageMoyen) + .ageMin(ageMin) + .ageMax(ageMax) + .nombreOrganisations(nombreOrganisations) + .nombreRegions(0) // TODO: Calculer depuis les adresses + .ancienneteMoyenne(ancienneteMoyenne) + .build(); + } + + // ======================================== + // MÉTHODES D'AUTOCOMPLÉTION (WOU/DRY) + // ======================================== + + /** + * Obtient la liste des villes distinctes depuis les adresses des membres + * Réutilisable pour autocomplétion (WOU/DRY) + */ + public List obtenirVillesDistinctes(String query) { + LOG.infof("Récupération des villes distinctes - query: %s", query); + + String jpql = "SELECT DISTINCT a.ville FROM Adresse a WHERE a.ville IS NOT NULL AND a.ville != ''"; + if (query != null && !query.trim().isEmpty()) { + jpql += " AND LOWER(a.ville) LIKE LOWER(:query)"; + } + jpql += " ORDER BY a.ville ASC"; + + TypedQuery typedQuery = entityManager.createQuery(jpql, String.class); + if (query != null && !query.trim().isEmpty()) { + typedQuery.setParameter("query", "%" + query.trim() + "%"); + } + typedQuery.setMaxResults(50); // Limiter à 50 résultats pour performance + + List villes = typedQuery.getResultList(); + LOG.infof("Trouvé %d villes distinctes", villes.size()); + return villes; + } + + /** + * Obtient la liste des professions distinctes depuis les membres + * Note: Si le champ profession n'existe pas dans Membre, retourne une liste vide + * Réutilisable pour autocomplétion (WOU/DRY) + */ + public List obtenirProfessionsDistinctes(String query) { + LOG.infof("Récupération des professions distinctes - query: %s", query); + + // TODO: Vérifier si le champ profession existe dans Membre + // Pour l'instant, retourner une liste vide car le champ n'existe pas + // Cette méthode peut être étendue si un champ profession est ajouté plus tard + LOG.warn("Le champ profession n'existe pas dans l'entité Membre. Retour d'une liste vide."); + return new ArrayList<>(); + } + + /** + * Exporte une sélection de membres en Excel (WOU/DRY - réutilise la logique d'export) + * + * @param membreIds Liste des IDs des membres à exporter + * @param format Format d'export (EXCEL, CSV, etc.) + * @return Données binaires du fichier Excel + */ + public byte[] exporterMembresSelectionnes(List membreIds, String format) { + LOG.infof("Export de %d membres sélectionnés - format: %s", membreIds.size(), format); + + if (membreIds == null || membreIds.isEmpty()) { + throw new IllegalArgumentException("La liste des membres ne peut pas être vide"); + } + + // Récupérer les membres + List membres = + membreIds.stream() + .map(id -> membreRepository.findByIdOptional(id)) + .filter(opt -> opt.isPresent()) + .map(java.util.Optional::get) + .collect(Collectors.toList()); + + // Convertir en DTOs + List membresDTO = convertToDTOList(membres); + + // Générer le fichier Excel (simplifié - à améliorer avec Apache POI) + // Pour l'instant, générer un CSV simple + StringBuilder csv = new StringBuilder(); + csv.append("Numéro;Nom;Prénom;Email;Téléphone;Statut;Date Adhésion\n"); + for (MembreDTO m : membresDTO) { + csv.append( + String.format( + "%s;%s;%s;%s;%s;%s;%s\n", + m.getNumeroMembre() != null ? m.getNumeroMembre() : "", + m.getNom() != null ? m.getNom() : "", + m.getPrenom() != null ? m.getPrenom() : "", + m.getEmail() != null ? m.getEmail() : "", + m.getTelephone() != null ? m.getTelephone() : "", + m.getStatut() != null ? m.getStatut() : "", + m.getDateAdhesion() != null ? m.getDateAdhesion().toString() : "")); + } + + return csv.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + /** + * Importe des membres depuis un fichier Excel ou CSV + */ + public MembreImportExportService.ResultatImport importerMembres( + InputStream fileInputStream, + String fileName, + UUID organisationId, + String typeMembreDefaut, + boolean mettreAJourExistants, + boolean ignorerErreurs) { + return membreImportExportService.importerMembres( + fileInputStream, fileName, organisationId, typeMembreDefaut, mettreAJourExistants, ignorerErreurs); + } + + /** + * Exporte des membres vers Excel + */ + public byte[] exporterVersExcel(List membres, List colonnesExport, boolean inclureHeaders, boolean formaterDates, boolean inclureStatistiques, String motDePasse) { + try { + return membreImportExportService.exporterVersExcel(membres, colonnesExport, inclureHeaders, formaterDates, inclureStatistiques, motDePasse); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'export Excel"); + throw new RuntimeException("Erreur lors de l'export Excel: " + e.getMessage(), e); + } + } + + /** + * Exporte des membres vers CSV + */ + public byte[] exporterVersCSV(List membres, List colonnesExport, boolean inclureHeaders, boolean formaterDates) { + try { + return membreImportExportService.exporterVersCSV(membres, colonnesExport, inclureHeaders, formaterDates); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de l'export CSV"); + throw new RuntimeException("Erreur lors de l'export CSV: " + e.getMessage(), e); + } + } + + /** + * Génère un modèle Excel pour l'import + */ + public byte[] genererModeleImport() { + try { + return membreImportExportService.genererModeleImport(); + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la génération du modèle"); + throw new RuntimeException("Erreur lors de la génération du modèle: " + e.getMessage(), e); + } + } + + /** + * Liste les membres pour l'export selon les filtres + */ + public List listerMembresPourExport( + UUID associationId, + String statut, + String type, + String dateAdhesionDebut, + String dateAdhesionFin) { + + List membres; + + if (associationId != null) { + TypedQuery query = entityManager.createQuery( + "SELECT m FROM Membre m WHERE m.organisation.id = :associationId", Membre.class); + query.setParameter("associationId", associationId); + membres = query.getResultList(); + } else { + membres = membreRepository.listAll(); + } + + // Filtrer par statut + if (statut != null && !statut.isEmpty()) { + boolean actif = "ACTIF".equals(statut); + membres = membres.stream() + .filter(m -> m.getActif() == actif) + .collect(Collectors.toList()); + } + + // Filtrer par dates d'adhésion + if (dateAdhesionDebut != null && !dateAdhesionDebut.isEmpty()) { + LocalDate dateDebut = LocalDate.parse(dateAdhesionDebut); + membres = membres.stream() + .filter(m -> m.getDateAdhesion() != null && !m.getDateAdhesion().isBefore(dateDebut)) + .collect(Collectors.toList()); + } + + if (dateAdhesionFin != null && !dateAdhesionFin.isEmpty()) { + LocalDate dateFin = LocalDate.parse(dateAdhesionFin); + membres = membres.stream() + .filter(m -> m.getDateAdhesion() != null && !m.getDateAdhesion().isAfter(dateFin)) + .collect(Collectors.toList()); + } + + return convertToDTOList(membres); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/NotificationHistoryService.java b/src/main/java/dev/lions/unionflow/server/service/NotificationHistoryService.java new file mode 100644 index 0000000..69fb4fc --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/NotificationHistoryService.java @@ -0,0 +1,322 @@ +package dev.lions.unionflow.server.service; + +import jakarta.enterprise.context.ApplicationScoped; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** Service pour gérer l'historique des notifications */ +@ApplicationScoped +public class NotificationHistoryService { + + private static final Logger LOG = Logger.getLogger(NotificationHistoryService.class); + + // Stockage temporaire en mémoire (à remplacer par une base de données) + private final Map> historiqueNotifications = + new ConcurrentHashMap<>(); + + /** Enregistre une notification dans l'historique */ + public void enregistrerNotification( + UUID utilisateurId, String type, String titre, String message, String canal, boolean succes) { + LOG.infof("Enregistrement de la notification %s pour l'utilisateur %s", type, utilisateurId); + + NotificationHistoryEntry entry = + NotificationHistoryEntry.builder() + .id(UUID.randomUUID()) + .utilisateurId(utilisateurId) + .type(type) + .titre(titre) + .message(message) + .canal(canal) + .dateEnvoi(LocalDateTime.now()) + .succes(succes) + .lu(false) + .build(); + + historiqueNotifications.computeIfAbsent(utilisateurId, k -> new ArrayList<>()).add(entry); + + // Limiter l'historique à 1000 notifications par utilisateur + List historique = historiqueNotifications.get(utilisateurId); + if (historique.size() > 1000) { + historique.sort(Comparator.comparing(NotificationHistoryEntry::getDateEnvoi).reversed()); + historiqueNotifications.put(utilisateurId, historique.subList(0, 1000)); + } + } + + /** Obtient l'historique des notifications d'un utilisateur */ + public List obtenirHistorique(UUID utilisateurId) { + LOG.infof( + "Récupération de l'historique des notifications pour l'utilisateur %s", utilisateurId); + + return historiqueNotifications.getOrDefault(utilisateurId, new ArrayList<>()).stream() + .sorted(Comparator.comparing(NotificationHistoryEntry::getDateEnvoi).reversed()) + .collect(Collectors.toList()); + } + + /** Obtient l'historique des notifications d'un utilisateur avec pagination */ + public List obtenirHistorique( + UUID utilisateurId, int page, int taille) { + List historique = obtenirHistorique(utilisateurId); + + int debut = page * taille; + int fin = Math.min(debut + taille, historique.size()); + + if (debut >= historique.size()) { + return new ArrayList<>(); + } + + return historique.subList(debut, fin); + } + + /** Marque une notification comme lue */ + public void marquerCommeLue(UUID utilisateurId, UUID notificationId) { + LOG.infof( + "Marquage de la notification %s comme lue pour l'utilisateur %s", + notificationId, utilisateurId); + + List historique = historiqueNotifications.get(utilisateurId); + if (historique != null) { + historique.stream() + .filter(entry -> entry.getId().equals(notificationId)) + .findFirst() + .ifPresent(entry -> entry.setLu(true)); + } + } + + /** Marque toutes les notifications comme lues */ + public void marquerToutesCommeLues(UUID utilisateurId) { + LOG.infof( + "Marquage de toutes les notifications comme lues pour l'utilisateur %s", utilisateurId); + + List historique = historiqueNotifications.get(utilisateurId); + if (historique != null) { + historique.forEach(entry -> entry.setLu(true)); + } + } + + /** Compte le nombre de notifications non lues */ + public long compterNotificationsNonLues(UUID utilisateurId) { + return obtenirHistorique(utilisateurId).stream().filter(entry -> !entry.isLu()).count(); + } + + /** Obtient les notifications non lues */ + public List obtenirNotificationsNonLues(UUID utilisateurId) { + return obtenirHistorique(utilisateurId).stream() + .filter(entry -> !entry.isLu()) + .collect(Collectors.toList()); + } + + /** Supprime les notifications anciennes (plus de 90 jours) */ + public void nettoyerHistorique() { + LOG.info("Nettoyage de l'historique des notifications"); + + LocalDateTime dateLimit = LocalDateTime.now().minusDays(90); + + for (Map.Entry> entry : + historiqueNotifications.entrySet()) { + List historique = entry.getValue(); + List historiqueFiltre = + historique.stream() + .filter(notification -> notification.getDateEnvoi().isAfter(dateLimit)) + .collect(Collectors.toList()); + + entry.setValue(historiqueFiltre); + } + } + + /** Obtient les statistiques des notifications pour un utilisateur */ + public Map obtenirStatistiques(UUID utilisateurId) { + List historique = obtenirHistorique(utilisateurId); + + Map stats = new HashMap<>(); + stats.put("total", historique.size()); + stats.put("nonLues", historique.stream().filter(entry -> !entry.isLu()).count()); + stats.put("succes", historique.stream().filter(NotificationHistoryEntry::isSucces).count()); + stats.put("echecs", historique.stream().filter(entry -> !entry.isSucces()).count()); + + // Statistiques par type + Map parType = + historique.stream() + .collect( + Collectors.groupingBy(NotificationHistoryEntry::getType, Collectors.counting())); + stats.put("parType", parType); + + // Statistiques par canal + Map parCanal = + historique.stream() + .collect( + Collectors.groupingBy(NotificationHistoryEntry::getCanal, Collectors.counting())); + stats.put("parCanal", parCanal); + + return stats; + } + + /** Classe interne pour représenter une entrée d'historique */ + public static class NotificationHistoryEntry { + private UUID id; + private UUID utilisateurId; + private String type; + private String titre; + private String message; + private String canal; + private LocalDateTime dateEnvoi; + private boolean succes; + private boolean lu; + + // Constructeurs + public NotificationHistoryEntry() {} + + private NotificationHistoryEntry(Builder builder) { + this.id = builder.id; + this.utilisateurId = builder.utilisateurId; + this.type = builder.type; + this.titre = builder.titre; + this.message = builder.message; + this.canal = builder.canal; + this.dateEnvoi = builder.dateEnvoi; + this.succes = builder.succes; + this.lu = builder.lu; + } + + public static Builder builder() { + return new Builder(); + } + + // Getters et Setters + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getUtilisateurId() { + return utilisateurId; + } + + public void setUtilisateurId(UUID utilisateurId) { + this.utilisateurId = utilisateurId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getTitre() { + return titre; + } + + public void setTitre(String titre) { + this.titre = titre; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getCanal() { + return canal; + } + + public void setCanal(String canal) { + this.canal = canal; + } + + public LocalDateTime getDateEnvoi() { + return dateEnvoi; + } + + public void setDateEnvoi(LocalDateTime dateEnvoi) { + this.dateEnvoi = dateEnvoi; + } + + public boolean isSucces() { + return succes; + } + + public void setSucces(boolean succes) { + this.succes = succes; + } + + public boolean isLu() { + return lu; + } + + public void setLu(boolean lu) { + this.lu = lu; + } + + // Builder + public static class Builder { + private UUID id; + private UUID utilisateurId; + private String type; + private String titre; + private String message; + private String canal; + private LocalDateTime dateEnvoi; + private boolean succes; + private boolean lu; + + public Builder id(UUID id) { + this.id = id; + return this; + } + + public Builder utilisateurId(UUID utilisateurId) { + this.utilisateurId = utilisateurId; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder titre(String titre) { + this.titre = titre; + return this; + } + + public Builder message(String message) { + this.message = message; + return this; + } + + public Builder canal(String canal) { + this.canal = canal; + return this; + } + + public Builder dateEnvoi(LocalDateTime dateEnvoi) { + this.dateEnvoi = dateEnvoi; + return this; + } + + public Builder succes(boolean succes) { + this.succes = succes; + return this; + } + + public Builder lu(boolean lu) { + this.lu = lu; + return this; + } + + public NotificationHistoryEntry build() { + return new NotificationHistoryEntry(this); + } + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/NotificationService.java b/src/main/java/dev/lions/unionflow/server/service/NotificationService.java new file mode 100644 index 0000000..26c3a21 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/NotificationService.java @@ -0,0 +1,352 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.notification.NotificationDTO; +import dev.lions.unionflow.server.api.dto.notification.TemplateNotificationDTO; +import dev.lions.unionflow.server.api.enums.notification.PrioriteNotification; +import dev.lions.unionflow.server.api.enums.notification.StatutNotification; +import dev.lions.unionflow.server.entity.*; +import dev.lions.unionflow.server.repository.*; +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** + * Service métier pour la gestion des notifications + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class NotificationService { + + private static final Logger LOG = Logger.getLogger(NotificationService.class); + + @Inject NotificationRepository notificationRepository; + + @Inject TemplateNotificationRepository templateNotificationRepository; + + @Inject MembreRepository membreRepository; + + @Inject OrganisationRepository organisationRepository; + + @Inject KeycloakService keycloakService; + + /** + * Crée un nouveau template de notification + * + * @param templateDTO DTO du template à créer + * @return DTO du template créé + */ + @Transactional + public TemplateNotificationDTO creerTemplate(TemplateNotificationDTO templateDTO) { + LOG.infof("Création d'un nouveau template: %s", templateDTO.getCode()); + + // Vérifier l'unicité du code + if (templateNotificationRepository.findByCode(templateDTO.getCode()).isPresent()) { + throw new IllegalArgumentException("Un template avec ce code existe déjà: " + templateDTO.getCode()); + } + + TemplateNotification template = convertToEntity(templateDTO); + template.setCreePar(keycloakService.getCurrentUserEmail()); + + templateNotificationRepository.persist(template); + LOG.infof("Template créé avec succès: ID=%s, Code=%s", template.getId(), template.getCode()); + + return convertToDTO(template); + } + + /** + * Crée une nouvelle notification + * + * @param notificationDTO DTO de la notification à créer + * @return DTO de la notification créée + */ + @Transactional + public NotificationDTO creerNotification(NotificationDTO notificationDTO) { + LOG.infof("Création d'une nouvelle notification: %s", notificationDTO.getTypeNotification()); + + Notification notification = convertToEntity(notificationDTO); + notification.setCreePar(keycloakService.getCurrentUserEmail()); + + notificationRepository.persist(notification); + LOG.infof("Notification créée avec succès: ID=%s", notification.getId()); + + return convertToDTO(notification); + } + + /** + * Marque une notification comme lue + * + * @param id ID de la notification + * @return DTO de la notification mise à jour + */ + @Transactional + public NotificationDTO marquerCommeLue(UUID id) { + LOG.infof("Marquage de la notification comme lue: ID=%s", id); + + Notification notification = + notificationRepository + .findNotificationById(id) + .orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id)); + + notification.setStatut(StatutNotification.LUE); + notification.setDateLecture(LocalDateTime.now()); + notification.setModifiePar(keycloakService.getCurrentUserEmail()); + + notificationRepository.persist(notification); + LOG.infof("Notification marquée comme lue: ID=%s", id); + + return convertToDTO(notification); + } + + /** + * Trouve une notification par son ID + * + * @param id ID de la notification + * @return DTO de la notification + */ + public NotificationDTO trouverNotificationParId(UUID id) { + return notificationRepository + .findNotificationById(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Notification non trouvée avec l'ID: " + id)); + } + + /** + * Liste toutes les notifications d'un membre + * + * @param membreId ID du membre + * @return Liste des notifications + */ + public List listerNotificationsParMembre(UUID membreId) { + return notificationRepository.findByMembreId(membreId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Liste les notifications non lues d'un membre + * + * @param membreId ID du membre + * @return Liste des notifications non lues + */ + public List listerNotificationsNonLuesParMembre(UUID membreId) { + return notificationRepository.findNonLuesByMembreId(membreId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Liste les notifications en attente d'envoi + * + * @return Liste des notifications en attente + */ + public List listerNotificationsEnAttenteEnvoi() { + return notificationRepository.findEnAttenteEnvoi().stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Envoie des notifications groupées à plusieurs membres (WOU/DRY) + * + * @param membreIds Liste des IDs des membres destinataires + * @param sujet Sujet de la notification + * @param corps Corps du message + * @param canaux Canaux d'envoi (EMAIL, SMS, etc.) + * @return Nombre de notifications créées + */ + @Transactional + public int envoyerNotificationsGroupees( + List membreIds, String sujet, String corps, List canaux) { + LOG.infof( + "Envoi de notifications groupées à %d membres - sujet: %s", membreIds.size(), sujet); + + if (membreIds == null || membreIds.isEmpty()) { + throw new IllegalArgumentException("La liste des membres ne peut pas être vide"); + } + + int notificationsCreees = 0; + for (UUID membreId : membreIds) { + try { + Membre membre = + membreRepository + .findByIdOptional(membreId) + .orElseThrow( + () -> + new IllegalArgumentException( + "Membre non trouvé avec l'ID: " + membreId)); + + Notification notification = new Notification(); + notification.setMembre(membre); + notification.setSujet(sujet); + notification.setCorps(corps); + notification.setTypeNotification( + dev.lions.unionflow.server.api.enums.notification.TypeNotification.IN_APP); + notification.setPriorite(PrioriteNotification.NORMALE); + notification.setStatut(StatutNotification.EN_ATTENTE); + notification.setDateEnvoiPrevue(java.time.LocalDateTime.now()); + notification.setCreePar(keycloakService.getCurrentUserEmail()); + + notificationRepository.persist(notification); + notificationsCreees++; + } catch (Exception e) { + LOG.warnf( + "Erreur lors de la création de la notification pour le membre %s: %s", + membreId, e.getMessage()); + } + } + + LOG.infof( + "%d notifications créées sur %d membres demandés", notificationsCreees, membreIds.size()); + return notificationsCreees; + } + + // ======================================== + // MÉTHODES PRIVÉES + // ======================================== + + /** Convertit une entité TemplateNotification en DTO */ + private TemplateNotificationDTO convertToDTO(TemplateNotification template) { + if (template == null) { + return null; + } + + TemplateNotificationDTO dto = new TemplateNotificationDTO(); + dto.setId(template.getId()); + dto.setCode(template.getCode()); + dto.setSujet(template.getSujet()); + dto.setCorpsTexte(template.getCorpsTexte()); + dto.setCorpsHtml(template.getCorpsHtml()); + dto.setVariablesDisponibles(template.getVariablesDisponibles()); + dto.setCanauxSupportes(template.getCanauxSupportes()); + dto.setLangue(template.getLangue()); + dto.setDescription(template.getDescription()); + dto.setDateCreation(template.getDateCreation()); + dto.setDateModification(template.getDateModification()); + dto.setActif(template.getActif()); + + return dto; + } + + /** Convertit un DTO en entité TemplateNotification */ + private TemplateNotification convertToEntity(TemplateNotificationDTO dto) { + if (dto == null) { + return null; + } + + TemplateNotification template = new TemplateNotification(); + template.setCode(dto.getCode()); + template.setSujet(dto.getSujet()); + template.setCorpsTexte(dto.getCorpsTexte()); + template.setCorpsHtml(dto.getCorpsHtml()); + template.setVariablesDisponibles(dto.getVariablesDisponibles()); + template.setCanauxSupportes(dto.getCanauxSupportes()); + template.setLangue(dto.getLangue() != null ? dto.getLangue() : "fr"); + template.setDescription(dto.getDescription()); + + return template; + } + + /** Convertit une entité Notification en DTO */ + private NotificationDTO convertToDTO(Notification notification) { + if (notification == null) { + return null; + } + + NotificationDTO dto = new NotificationDTO(); + dto.setId(notification.getId()); + dto.setTypeNotification(notification.getTypeNotification()); + dto.setPriorite(notification.getPriorite()); + dto.setStatut(notification.getStatut()); + dto.setSujet(notification.getSujet()); + dto.setCorps(notification.getCorps()); + dto.setDateEnvoiPrevue(notification.getDateEnvoiPrevue()); + dto.setDateEnvoi(notification.getDateEnvoi()); + dto.setDateLecture(notification.getDateLecture()); + dto.setNombreTentatives(notification.getNombreTentatives()); + dto.setMessageErreur(notification.getMessageErreur()); + dto.setDonneesAdditionnelles(notification.getDonneesAdditionnelles()); + + if (notification.getMembre() != null) { + dto.setMembreId(notification.getMembre().getId()); + } + if (notification.getOrganisation() != null) { + dto.setOrganisationId(notification.getOrganisation().getId()); + } + if (notification.getTemplate() != null) { + dto.setTemplateId(notification.getTemplate().getId()); + } + + dto.setDateCreation(notification.getDateCreation()); + dto.setDateModification(notification.getDateModification()); + dto.setActif(notification.getActif()); + + return dto; + } + + /** Convertit un DTO en entité Notification */ + private Notification convertToEntity(NotificationDTO dto) { + if (dto == null) { + return null; + } + + Notification notification = new Notification(); + notification.setTypeNotification(dto.getTypeNotification()); + notification.setPriorite( + dto.getPriorite() != null ? dto.getPriorite() : PrioriteNotification.NORMALE); + notification.setStatut( + dto.getStatut() != null ? dto.getStatut() : StatutNotification.EN_ATTENTE); + notification.setSujet(dto.getSujet()); + notification.setCorps(dto.getCorps()); + notification.setDateEnvoiPrevue( + dto.getDateEnvoiPrevue() != null ? dto.getDateEnvoiPrevue() : LocalDateTime.now()); + notification.setDateEnvoi(dto.getDateEnvoi()); + notification.setDateLecture(dto.getDateLecture()); + notification.setNombreTentatives(dto.getNombreTentatives() != null ? dto.getNombreTentatives() : 0); + notification.setMessageErreur(dto.getMessageErreur()); + notification.setDonneesAdditionnelles(dto.getDonneesAdditionnelles()); + + // Relations + if (dto.getMembreId() != null) { + Membre membre = + membreRepository + .findByIdOptional(dto.getMembreId()) + .orElseThrow( + () -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId())); + notification.setMembre(membre); + } + + if (dto.getOrganisationId() != null) { + Organisation org = + organisationRepository + .findByIdOptional(dto.getOrganisationId()) + .orElseThrow( + () -> + new NotFoundException( + "Organisation non trouvée avec l'ID: " + dto.getOrganisationId())); + notification.setOrganisation(org); + } + + if (dto.getTemplateId() != null) { + TemplateNotification template = + templateNotificationRepository + .findTemplateNotificationById(dto.getTemplateId()) + .orElseThrow( + () -> + new NotFoundException( + "Template non trouvé avec l'ID: " + dto.getTemplateId())); + notification.setTemplate(template); + } + + return notification; + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java b/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java new file mode 100644 index 0000000..26f7dad --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/OrganisationService.java @@ -0,0 +1,443 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO; +import dev.lions.unionflow.server.api.enums.organisation.StatutOrganisation; +import dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Service métier pour la gestion des organisations + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@ApplicationScoped +public class OrganisationService { + + private static final Logger LOG = Logger.getLogger(OrganisationService.class); + + @Inject OrganisationRepository organisationRepository; + + /** + * Crée une nouvelle organisation + * + * @param organisation l'organisation à créer + * @return l'organisation créée + */ + @Transactional + public Organisation creerOrganisation(Organisation organisation) { + LOG.infof("Création d'une nouvelle organisation: %s", organisation.getNom()); + + // Vérifier l'unicité de l'email + if (organisationRepository.findByEmail(organisation.getEmail()).isPresent()) { + throw new IllegalArgumentException("Une organisation avec cet email existe déjà"); + } + + // Vérifier l'unicité du nom + if (organisationRepository.findByNom(organisation.getNom()).isPresent()) { + throw new IllegalArgumentException("Une organisation avec ce nom existe déjà"); + } + + // Vérifier l'unicité du numéro d'enregistrement si fourni + if (organisation.getNumeroEnregistrement() != null + && !organisation.getNumeroEnregistrement().isEmpty()) { + if (organisationRepository + .findByNumeroEnregistrement(organisation.getNumeroEnregistrement()) + .isPresent()) { + throw new IllegalArgumentException( + "Une organisation avec ce numéro d'enregistrement existe déjà"); + } + } + + // Définir les valeurs par défaut + if (organisation.getStatut() == null) { + organisation.setStatut("ACTIVE"); + } + if (organisation.getTypeOrganisation() == null) { + organisation.setTypeOrganisation("ASSOCIATION"); + } + + organisationRepository.persist(organisation); + LOG.infof( + "Organisation créée avec succès: ID=%s, Nom=%s", organisation.getId(), organisation.getNom()); + + return organisation; + } + + /** + * Met à jour une organisation existante + * + * @param id l'ID de l'organisation + * @param organisationMiseAJour les données de mise à jour + * @param utilisateur l'utilisateur effectuant la modification + * @return l'organisation mise à jour + */ + @Transactional + public Organisation mettreAJourOrganisation( + UUID id, Organisation organisationMiseAJour, String utilisateur) { + LOG.infof("Mise à jour de l'organisation ID: %s", id); + + Organisation organisation = + organisationRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id)); + + // Vérifier l'unicité de l'email si modifié + if (!organisation.getEmail().equals(organisationMiseAJour.getEmail())) { + if (organisationRepository.findByEmail(organisationMiseAJour.getEmail()).isPresent()) { + throw new IllegalArgumentException("Une organisation avec cet email existe déjà"); + } + organisation.setEmail(organisationMiseAJour.getEmail()); + } + + // Vérifier l'unicité du nom si modifié + if (!organisation.getNom().equals(organisationMiseAJour.getNom())) { + if (organisationRepository.findByNom(organisationMiseAJour.getNom()).isPresent()) { + throw new IllegalArgumentException("Une organisation avec ce nom existe déjà"); + } + organisation.setNom(organisationMiseAJour.getNom()); + } + + // Mettre à jour les autres champs + organisation.setNomCourt(organisationMiseAJour.getNomCourt()); + organisation.setDescription(organisationMiseAJour.getDescription()); + organisation.setTelephone(organisationMiseAJour.getTelephone()); + organisation.setAdresse(organisationMiseAJour.getAdresse()); + organisation.setVille(organisationMiseAJour.getVille()); + organisation.setCodePostal(organisationMiseAJour.getCodePostal()); + organisation.setRegion(organisationMiseAJour.getRegion()); + organisation.setPays(organisationMiseAJour.getPays()); + organisation.setSiteWeb(organisationMiseAJour.getSiteWeb()); + organisation.setObjectifs(organisationMiseAJour.getObjectifs()); + organisation.setActivitesPrincipales(organisationMiseAJour.getActivitesPrincipales()); + + organisation.marquerCommeModifie(utilisateur); + + LOG.infof("Organisation mise à jour avec succès: ID=%s", id); + return organisation; + } + + /** + * Supprime une organisation + * + * @param id l'UUID de l'organisation + * @param utilisateur l'utilisateur effectuant la suppression + */ + @Transactional + public void supprimerOrganisation(UUID id, String utilisateur) { + LOG.infof("Suppression de l'organisation ID: %s", id); + + Organisation organisation = + organisationRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id)); + + // Vérifier qu'il n'y a pas de membres actifs + if (organisation.getNombreMembres() > 0) { + throw new IllegalStateException( + "Impossible de supprimer une organisation avec des membres actifs"); + } + + // Soft delete - marquer comme inactive + organisation.setActif(false); + organisation.setStatut("DISSOUTE"); + organisation.marquerCommeModifie(utilisateur); + + LOG.infof("Organisation supprimée (soft delete) avec succès: ID=%s", id); + } + + /** + * Trouve une organisation par son ID + * + * @param id l'UUID de l'organisation + * @return Optional contenant l'organisation si trouvée + */ + public Optional trouverParId(UUID id) { + return organisationRepository.findByIdOptional(id); + } + + /** + * Trouve une organisation par son email + * + * @param email l'email de l'organisation + * @return Optional contenant l'organisation si trouvée + */ + public Optional trouverParEmail(String email) { + return organisationRepository.findByEmail(email); + } + + /** + * Liste toutes les organisations actives + * + * @return liste des organisations actives + */ + public List listerOrganisationsActives() { + return organisationRepository.findAllActives(); + } + + /** + * Liste toutes les organisations actives avec pagination + * + * @param page numéro de page + * @param size taille de la page + * @return liste paginée des organisations actives + */ + public List listerOrganisationsActives(int page, int size) { + return organisationRepository.findAllActives(Page.of(page, size), Sort.by("nom").ascending()); + } + + /** + * Recherche d'organisations par nom + * + * @param recherche terme de recherche + * @param page numéro de page + * @param size taille de la page + * @return liste paginée des organisations correspondantes + */ + public List rechercherOrganisations(String recherche, int page, int size) { + return organisationRepository.findByNomOrNomCourt( + recherche, Page.of(page, size), Sort.by("nom").ascending()); + } + + /** + * Recherche avancée d'organisations + * + * @param nom nom (optionnel) + * @param typeOrganisation type (optionnel) + * @param statut statut (optionnel) + * @param ville ville (optionnel) + * @param region région (optionnel) + * @param pays pays (optionnel) + * @param page numéro de page + * @param size taille de la page + * @return liste filtrée des organisations + */ + public List rechercheAvancee( + String nom, + String typeOrganisation, + String statut, + String ville, + String region, + String pays, + int page, + int size) { + return organisationRepository.rechercheAvancee( + nom, typeOrganisation, statut, ville, region, pays, Page.of(page, size)); + } + + /** + * Active une organisation + * + * @param id l'ID de l'organisation + * @param utilisateur l'utilisateur effectuant l'activation + * @return l'organisation activée + */ + @Transactional + public Organisation activerOrganisation(UUID id, String utilisateur) { + LOG.infof("Activation de l'organisation ID: %s", id); + + Organisation organisation = + organisationRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id)); + + organisation.activer(utilisateur); + + LOG.infof("Organisation activée avec succès: ID=%s", id); + return organisation; + } + + /** + * Suspend une organisation + * + * @param id l'UUID de l'organisation + * @param utilisateur l'utilisateur effectuant la suspension + * @return l'organisation suspendue + */ + @Transactional + public Organisation suspendreOrganisation(UUID id, String utilisateur) { + LOG.infof("Suspension de l'organisation ID: %s", id); + + Organisation organisation = + organisationRepository + .findByIdOptional(id) + .orElseThrow(() -> new NotFoundException("Organisation non trouvée avec l'ID: " + id)); + + organisation.suspendre(utilisateur); + + LOG.infof("Organisation suspendue avec succès: ID=%s", id); + return organisation; + } + + /** + * Obtient les statistiques des organisations + * + * @return map contenant les statistiques + */ + public Map obtenirStatistiques() { + LOG.info("Calcul des statistiques des organisations"); + + long totalOrganisations = organisationRepository.count(); + long organisationsActives = organisationRepository.countActives(); + long organisationsInactives = totalOrganisations - organisationsActives; + long nouvellesOrganisations30Jours = + organisationRepository.countNouvellesOrganisations(LocalDate.now().minusDays(30)); + + return Map.of( + "totalOrganisations", totalOrganisations, + "organisationsActives", organisationsActives, + "organisationsInactives", organisationsInactives, + "nouvellesOrganisations30Jours", nouvellesOrganisations30Jours, + "tauxActivite", + totalOrganisations > 0 ? (organisationsActives * 100.0 / totalOrganisations) : 0.0, + "timestamp", LocalDateTime.now()); + } + + /** + * Convertit une entité Organisation en DTO + * + * @param organisation l'entité à convertir + * @return le DTO correspondant + */ + public OrganisationDTO convertToDTO(Organisation organisation) { + if (organisation == null) { + return null; + } + + OrganisationDTO dto = new OrganisationDTO(); + + // Conversion de l'ID UUID vers UUID (pas de conversion nécessaire maintenant) + dto.setId(organisation.getId()); + + // Informations de base + dto.setNom(organisation.getNom()); + dto.setNomCourt(organisation.getNomCourt()); + dto.setDescription(organisation.getDescription()); + dto.setEmail(organisation.getEmail()); + dto.setTelephone(organisation.getTelephone()); + dto.setTelephoneSecondaire(organisation.getTelephoneSecondaire()); + dto.setEmailSecondaire(organisation.getEmailSecondaire()); + dto.setAdresse(organisation.getAdresse()); + dto.setVille(organisation.getVille()); + dto.setCodePostal(organisation.getCodePostal()); + dto.setRegion(organisation.getRegion()); + dto.setPays(organisation.getPays()); + dto.setLatitude(organisation.getLatitude()); + dto.setLongitude(organisation.getLongitude()); + dto.setSiteWeb(organisation.getSiteWeb()); + dto.setLogo(organisation.getLogo()); + dto.setReseauxSociaux(organisation.getReseauxSociaux()); + dto.setObjectifs(organisation.getObjectifs()); + dto.setActivitesPrincipales(organisation.getActivitesPrincipales()); + dto.setNombreMembres(organisation.getNombreMembres()); + dto.setNombreAdministrateurs(organisation.getNombreAdministrateurs()); + dto.setBudgetAnnuel(organisation.getBudgetAnnuel()); + dto.setDevise(organisation.getDevise()); + dto.setDateFondation(organisation.getDateFondation()); + dto.setNumeroEnregistrement(organisation.getNumeroEnregistrement()); + dto.setNiveauHierarchique(organisation.getNiveauHierarchique()); + + // Conversion de l'organisation parente (UUID → UUID, pas de conversion nécessaire) + if (organisation.getOrganisationParenteId() != null) { + dto.setOrganisationParenteId(organisation.getOrganisationParenteId()); + } + + // Conversion du type d'organisation (String → Enum) + if (organisation.getTypeOrganisation() != null) { + try { + dto.setTypeOrganisation( + TypeOrganisation.valueOf(organisation.getTypeOrganisation().toUpperCase())); + } catch (IllegalArgumentException e) { + // Valeur par défaut si la conversion échoue + LOG.warnf( + "Type d'organisation inconnu: %s, utilisation de ASSOCIATION par défaut", + organisation.getTypeOrganisation()); + dto.setTypeOrganisation(TypeOrganisation.ASSOCIATION); + } + } else { + dto.setTypeOrganisation(TypeOrganisation.ASSOCIATION); + } + + // Conversion du statut (String → Enum) + if (organisation.getStatut() != null) { + try { + dto.setStatut( + StatutOrganisation.valueOf(organisation.getStatut().toUpperCase())); + } catch (IllegalArgumentException e) { + // Valeur par défaut si la conversion échoue + LOG.warnf( + "Statut d'organisation inconnu: %s, utilisation de ACTIVE par défaut", + organisation.getStatut()); + dto.setStatut(StatutOrganisation.ACTIVE); + } + } else { + dto.setStatut(StatutOrganisation.ACTIVE); + } + + // Champs de base DTO + dto.setDateCreation(organisation.getDateCreation()); + dto.setDateModification(organisation.getDateModification()); + dto.setActif(organisation.getActif()); + dto.setVersion(organisation.getVersion() != null ? organisation.getVersion() : 0L); + + // Champs par défaut + dto.setOrganisationPublique( + organisation.getOrganisationPublique() != null + ? organisation.getOrganisationPublique() + : true); + dto.setAccepteNouveauxMembres( + organisation.getAccepteNouveauxMembres() != null + ? organisation.getAccepteNouveauxMembres() + : true); + dto.setCotisationObligatoire( + organisation.getCotisationObligatoire() != null + ? organisation.getCotisationObligatoire() + : false); + dto.setMontantCotisationAnnuelle(organisation.getMontantCotisationAnnuelle()); + + return dto; + } + + /** + * Convertit un DTO en entité Organisation + * + * @param dto le DTO à convertir + * @return l'entité correspondante + */ + public Organisation convertFromDTO(OrganisationDTO dto) { + if (dto == null) { + return null; + } + + return Organisation.builder() + .nom(dto.getNom()) + .nomCourt(dto.getNomCourt()) + .description(dto.getDescription()) + .email(dto.getEmail()) + .telephone(dto.getTelephone()) + .adresse(dto.getAdresse()) + .ville(dto.getVille()) + .codePostal(dto.getCodePostal()) + .region(dto.getRegion()) + .pays(dto.getPays()) + .siteWeb(dto.getSiteWeb()) + .objectifs(dto.getObjectifs()) + .activitesPrincipales(dto.getActivitesPrincipales()) + .build(); + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/PaiementService.java b/src/main/java/dev/lions/unionflow/server/service/PaiementService.java new file mode 100644 index 0000000..6c5bfb4 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/PaiementService.java @@ -0,0 +1,309 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.paiement.PaiementDTO; +import dev.lions.unionflow.server.api.enums.paiement.StatutPaiement; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Paiement; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.PaiementRepository; +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** + * Service métier pour la gestion des paiements + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class PaiementService { + + private static final Logger LOG = Logger.getLogger(PaiementService.class); + + @Inject PaiementRepository paiementRepository; + + @Inject MembreRepository membreRepository; + + @Inject KeycloakService keycloakService; + + /** + * Crée un nouveau paiement + * + * @param paiementDTO DTO du paiement à créer + * @return DTO du paiement créé + */ + @Transactional + public PaiementDTO creerPaiement(PaiementDTO paiementDTO) { + LOG.infof("Création d'un nouveau paiement: %s", paiementDTO.getNumeroReference()); + + Paiement paiement = convertToEntity(paiementDTO); + paiement.setCreePar(keycloakService.getCurrentUserEmail()); + + paiementRepository.persist(paiement); + LOG.infof("Paiement créé avec succès: ID=%s, Référence=%s", paiement.getId(), paiement.getNumeroReference()); + + return convertToDTO(paiement); + } + + /** + * Met à jour un paiement existant + * + * @param id ID du paiement + * @param paiementDTO DTO avec les modifications + * @return DTO du paiement mis à jour + */ + @Transactional + public PaiementDTO mettreAJourPaiement(UUID id, PaiementDTO paiementDTO) { + LOG.infof("Mise à jour du paiement ID: %s", id); + + Paiement paiement = + paiementRepository + .findPaiementById(id) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id)); + + if (!paiement.peutEtreModifie()) { + throw new IllegalStateException("Le paiement ne peut plus être modifié (statut finalisé)"); + } + + updateFromDTO(paiement, paiementDTO); + paiement.setModifiePar(keycloakService.getCurrentUserEmail()); + + paiementRepository.persist(paiement); + LOG.infof("Paiement mis à jour avec succès: ID=%s", id); + + return convertToDTO(paiement); + } + + /** + * Valide un paiement + * + * @param id ID du paiement + * @return DTO du paiement validé + */ + @Transactional + public PaiementDTO validerPaiement(UUID id) { + LOG.infof("Validation du paiement ID: %s", id); + + Paiement paiement = + paiementRepository + .findPaiementById(id) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id)); + + if (paiement.isValide()) { + LOG.warnf("Le paiement ID=%s est déjà validé", id); + return convertToDTO(paiement); + } + + paiement.setStatutPaiement(StatutPaiement.VALIDE); + paiement.setDateValidation(LocalDateTime.now()); + paiement.setValidateur(keycloakService.getCurrentUserEmail()); + paiement.setModifiePar(keycloakService.getCurrentUserEmail()); + + paiementRepository.persist(paiement); + LOG.infof("Paiement validé avec succès: ID=%s", id); + + return convertToDTO(paiement); + } + + /** + * Annule un paiement + * + * @param id ID du paiement + * @return DTO du paiement annulé + */ + @Transactional + public PaiementDTO annulerPaiement(UUID id) { + LOG.infof("Annulation du paiement ID: %s", id); + + Paiement paiement = + paiementRepository + .findPaiementById(id) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id)); + + if (!paiement.peutEtreModifie()) { + throw new IllegalStateException("Le paiement ne peut plus être annulé (statut finalisé)"); + } + + paiement.setStatutPaiement(StatutPaiement.ANNULE); + paiement.setModifiePar(keycloakService.getCurrentUserEmail()); + + paiementRepository.persist(paiement); + LOG.infof("Paiement annulé avec succès: ID=%s", id); + + return convertToDTO(paiement); + } + + /** + * Trouve un paiement par son ID + * + * @param id ID du paiement + * @return DTO du paiement + */ + public PaiementDTO trouverParId(UUID id) { + return paiementRepository + .findPaiementById(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec l'ID: " + id)); + } + + /** + * Trouve un paiement par son numéro de référence + * + * @param numeroReference Numéro de référence + * @return DTO du paiement + */ + public PaiementDTO trouverParNumeroReference(String numeroReference) { + return paiementRepository + .findByNumeroReference(numeroReference) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Paiement non trouvé avec la référence: " + numeroReference)); + } + + /** + * Liste tous les paiements d'un membre + * + * @param membreId ID du membre + * @return Liste des paiements + */ + public List listerParMembre(UUID membreId) { + return paiementRepository.findByMembreId(membreId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Calcule le montant total des paiements validés dans une période + * + * @param dateDebut Date de début + * @param dateFin Date de fin + * @return Montant total + */ + public BigDecimal calculerMontantTotalValides(LocalDateTime dateDebut, LocalDateTime dateFin) { + return paiementRepository.calculerMontantTotalValides(dateDebut, dateFin); + } + + // ======================================== + // MÉTHODES PRIVÉES + // ======================================== + + /** Convertit une entité en DTO */ + private PaiementDTO convertToDTO(Paiement paiement) { + if (paiement == null) { + return null; + } + + PaiementDTO dto = new PaiementDTO(); + dto.setId(paiement.getId()); + dto.setNumeroReference(paiement.getNumeroReference()); + dto.setMontant(paiement.getMontant()); + dto.setCodeDevise(paiement.getCodeDevise()); + dto.setMethodePaiement(paiement.getMethodePaiement()); + dto.setStatutPaiement(paiement.getStatutPaiement()); + dto.setDatePaiement(paiement.getDatePaiement()); + dto.setDateValidation(paiement.getDateValidation()); + dto.setValidateur(paiement.getValidateur()); + dto.setReferenceExterne(paiement.getReferenceExterne()); + dto.setUrlPreuve(paiement.getUrlPreuve()); + dto.setCommentaire(paiement.getCommentaire()); + dto.setIpAddress(paiement.getIpAddress()); + dto.setUserAgent(paiement.getUserAgent()); + + if (paiement.getMembre() != null) { + dto.setMembreId(paiement.getMembre().getId()); + } + if (paiement.getTransactionWave() != null) { + dto.setTransactionWaveId(paiement.getTransactionWave().getId()); + } + + dto.setDateCreation(paiement.getDateCreation()); + dto.setDateModification(paiement.getDateModification()); + dto.setActif(paiement.getActif()); + + return dto; + } + + /** Convertit un DTO en entité */ + private Paiement convertToEntity(PaiementDTO dto) { + if (dto == null) { + return null; + } + + Paiement paiement = new Paiement(); + paiement.setNumeroReference(dto.getNumeroReference()); + paiement.setMontant(dto.getMontant()); + paiement.setCodeDevise(dto.getCodeDevise()); + paiement.setMethodePaiement(dto.getMethodePaiement()); + paiement.setStatutPaiement(dto.getStatutPaiement() != null ? dto.getStatutPaiement() : StatutPaiement.EN_ATTENTE); + paiement.setDatePaiement(dto.getDatePaiement()); + paiement.setDateValidation(dto.getDateValidation()); + paiement.setValidateur(dto.getValidateur()); + paiement.setReferenceExterne(dto.getReferenceExterne()); + paiement.setUrlPreuve(dto.getUrlPreuve()); + paiement.setCommentaire(dto.getCommentaire()); + paiement.setIpAddress(dto.getIpAddress()); + paiement.setUserAgent(dto.getUserAgent()); + + // Relation Membre + if (dto.getMembreId() != null) { + Membre membre = + membreRepository + .findByIdOptional(dto.getMembreId()) + .orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId())); + paiement.setMembre(membre); + } + + // Relation TransactionWave sera gérée par WaveService + + return paiement; + } + + /** Met à jour une entité à partir d'un DTO */ + private void updateFromDTO(Paiement paiement, PaiementDTO dto) { + if (dto.getMontant() != null) { + paiement.setMontant(dto.getMontant()); + } + if (dto.getCodeDevise() != null) { + paiement.setCodeDevise(dto.getCodeDevise()); + } + if (dto.getMethodePaiement() != null) { + paiement.setMethodePaiement(dto.getMethodePaiement()); + } + if (dto.getStatutPaiement() != null) { + paiement.setStatutPaiement(dto.getStatutPaiement()); + } + if (dto.getDatePaiement() != null) { + paiement.setDatePaiement(dto.getDatePaiement()); + } + if (dto.getDateValidation() != null) { + paiement.setDateValidation(dto.getDateValidation()); + } + if (dto.getValidateur() != null) { + paiement.setValidateur(dto.getValidateur()); + } + if (dto.getReferenceExterne() != null) { + paiement.setReferenceExterne(dto.getReferenceExterne()); + } + if (dto.getUrlPreuve() != null) { + paiement.setUrlPreuve(dto.getUrlPreuve()); + } + if (dto.getCommentaire() != null) { + paiement.setCommentaire(dto.getCommentaire()); + } + if (dto.getIpAddress() != null) { + paiement.setIpAddress(dto.getIpAddress()); + } + if (dto.getUserAgent() != null) { + paiement.setUserAgent(dto.getUserAgent()); + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/PermissionService.java b/src/main/java/dev/lions/unionflow/server/service/PermissionService.java new file mode 100644 index 0000000..f927c61 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/PermissionService.java @@ -0,0 +1,165 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.entity.Permission; +import dev.lions.unionflow.server.repository.PermissionRepository; +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import java.util.List; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Service métier pour la gestion des permissions + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class PermissionService { + + private static final Logger LOG = Logger.getLogger(PermissionService.class); + + @Inject PermissionRepository permissionRepository; + + @Inject KeycloakService keycloakService; + + /** + * Crée une nouvelle permission + * + * @param permission Permission à créer + * @return Permission créée + */ + @Transactional + public Permission creerPermission(Permission permission) { + LOG.infof("Création d'une nouvelle permission: %s", permission.getCode()); + + // Vérifier l'unicité du code + if (permissionRepository.findByCode(permission.getCode()).isPresent()) { + throw new IllegalArgumentException( + "Une permission avec ce code existe déjà: " + permission.getCode()); + } + + // Générer le code si non fourni + if (permission.getCode() == null || permission.getCode().isEmpty()) { + permission.setCode( + Permission.genererCode( + permission.getModule(), permission.getRessource(), permission.getAction())); + } + + // Métadonnées + permission.setCreePar(keycloakService.getCurrentUserEmail()); + + permissionRepository.persist(permission); + LOG.infof( + "Permission créée avec succès: ID=%s, Code=%s", + permission.getId(), permission.getCode()); + + return permission; + } + + /** + * Met à jour une permission existante + * + * @param id ID de la permission + * @param permissionModifiee Permission avec les modifications + * @return Permission mise à jour + */ + @Transactional + public Permission mettreAJourPermission(UUID id, Permission permissionModifiee) { + LOG.infof("Mise à jour de la permission ID: %s", id); + + Permission permission = + permissionRepository + .findPermissionById(id) + .orElseThrow(() -> new NotFoundException("Permission non trouvée avec l'ID: " + id)); + + // Mise à jour + permission.setCode(permissionModifiee.getCode()); + permission.setModule(permissionModifiee.getModule()); + permission.setRessource(permissionModifiee.getRessource()); + permission.setAction(permissionModifiee.getAction()); + permission.setLibelle(permissionModifiee.getLibelle()); + permission.setDescription(permissionModifiee.getDescription()); + permission.setModifiePar(keycloakService.getCurrentUserEmail()); + + permissionRepository.persist(permission); + LOG.infof("Permission mise à jour avec succès: ID=%s", id); + + return permission; + } + + /** + * Trouve une permission par son ID + * + * @param id ID de la permission + * @return Permission ou null + */ + public Permission trouverParId(UUID id) { + return permissionRepository.findPermissionById(id).orElse(null); + } + + /** + * Trouve une permission par son code + * + * @param code Code de la permission + * @return Permission ou null + */ + public Permission trouverParCode(String code) { + return permissionRepository.findByCode(code).orElse(null); + } + + /** + * Liste les permissions par module + * + * @param module Nom du module + * @return Liste des permissions + */ + public List listerParModule(String module) { + return permissionRepository.findByModule(module); + } + + /** + * Liste les permissions par ressource + * + * @param ressource Nom de la ressource + * @return Liste des permissions + */ + public List listerParRessource(String ressource) { + return permissionRepository.findByRessource(ressource); + } + + /** + * Liste toutes les permissions actives + * + * @return Liste des permissions actives + */ + public List listerToutesActives() { + return permissionRepository.findAllActives(); + } + + /** + * Supprime (désactive) une permission + * + * @param id ID de la permission + */ + @Transactional + public void supprimerPermission(UUID id) { + LOG.infof("Suppression de la permission ID: %s", id); + + Permission permission = + permissionRepository + .findPermissionById(id) + .orElseThrow(() -> new NotFoundException("Permission non trouvée avec l'ID: " + id)); + + permission.setActif(false); + permission.setModifiePar(keycloakService.getCurrentUserEmail()); + + permissionRepository.persist(permission); + LOG.infof("Permission supprimée avec succès: ID=%s", id); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/service/PreferencesNotificationService.java b/src/main/java/dev/lions/unionflow/server/service/PreferencesNotificationService.java new file mode 100644 index 0000000..65c00c6 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/PreferencesNotificationService.java @@ -0,0 +1,140 @@ +package dev.lions.unionflow.server.service; + +import jakarta.enterprise.context.ApplicationScoped; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** Service pour gérer les préférences de notification des utilisateurs */ +@ApplicationScoped +public class PreferencesNotificationService { + + private static final Logger LOG = Logger.getLogger(PreferencesNotificationService.class); + + // Stockage temporaire en mémoire (à remplacer par une base de données) + private final Map> preferencesUtilisateurs = new HashMap<>(); + + /** Obtient les préférences de notification d'un utilisateur */ + public Map obtenirPreferences(UUID utilisateurId) { + LOG.infof("Récupération des préférences de notification pour l'utilisateur %s", utilisateurId); + + return preferencesUtilisateurs.getOrDefault(utilisateurId, getPreferencesParDefaut()); + } + + /** Met à jour les préférences de notification d'un utilisateur */ + public void mettreAJourPreferences(UUID utilisateurId, Map preferences) { + LOG.infof("Mise à jour des préférences de notification pour l'utilisateur %s", utilisateurId); + + preferencesUtilisateurs.put(utilisateurId, new HashMap<>(preferences)); + } + + /** Vérifie si un utilisateur souhaite recevoir un type de notification */ + public boolean accepteNotification(UUID utilisateurId, String typeNotification) { + Map preferences = obtenirPreferences(utilisateurId); + return preferences.getOrDefault(typeNotification, true); + } + + /** Active un type de notification pour un utilisateur */ + public void activerNotification(UUID utilisateurId, String typeNotification) { + LOG.infof( + "Activation de la notification %s pour l'utilisateur %s", typeNotification, utilisateurId); + + Map preferences = obtenirPreferences(utilisateurId); + preferences.put(typeNotification, true); + mettreAJourPreferences(utilisateurId, preferences); + } + + /** Désactive un type de notification pour un utilisateur */ + public void desactiverNotification(UUID utilisateurId, String typeNotification) { + LOG.infof( + "Désactivation de la notification %s pour l'utilisateur %s", + typeNotification, utilisateurId); + + Map preferences = obtenirPreferences(utilisateurId); + preferences.put(typeNotification, false); + mettreAJourPreferences(utilisateurId, preferences); + } + + /** Réinitialise les préférences d'un utilisateur aux valeurs par défaut */ + public void reinitialiserPreferences(UUID utilisateurId) { + LOG.infof("Réinitialisation des préférences pour l'utilisateur %s", utilisateurId); + + mettreAJourPreferences(utilisateurId, getPreferencesParDefaut()); + } + + /** Obtient les préférences par défaut */ + private Map getPreferencesParDefaut() { + Map preferences = new HashMap<>(); + + // Notifications générales + preferences.put("NOUVELLE_COTISATION", true); + preferences.put("RAPPEL_COTISATION", true); + preferences.put("COTISATION_RETARD", true); + + // Notifications d'événements + preferences.put("NOUVEL_EVENEMENT", true); + preferences.put("RAPPEL_EVENEMENT", true); + preferences.put("MODIFICATION_EVENEMENT", true); + preferences.put("ANNULATION_EVENEMENT", true); + + // Notifications de solidarité + preferences.put("NOUVELLE_DEMANDE_AIDE", true); + preferences.put("DEMANDE_AIDE_APPROUVEE", true); + preferences.put("DEMANDE_AIDE_REJETEE", true); + preferences.put("NOUVELLE_PROPOSITION_AIDE", true); + + // Notifications administratives + preferences.put("NOUVEAU_MEMBRE", false); + preferences.put("MODIFICATION_PROFIL", false); + preferences.put("RAPPORT_MENSUEL", true); + + // Notifications push + preferences.put("PUSH_MOBILE", true); + preferences.put("EMAIL", true); + preferences.put("SMS", false); + + return preferences; + } + + /** Obtient tous les utilisateurs qui acceptent un type de notification */ + public Map obtenirUtilisateursAcceptantNotification(String typeNotification) { + LOG.infof("Recherche des utilisateurs acceptant la notification %s", typeNotification); + + Map utilisateursAcceptant = new HashMap<>(); + + for (Map.Entry> entry : preferencesUtilisateurs.entrySet()) { + UUID utilisateurId = entry.getKey(); + Map preferences = entry.getValue(); + + if (preferences.getOrDefault(typeNotification, true)) { + utilisateursAcceptant.put(utilisateurId, true); + } + } + + return utilisateursAcceptant; + } + + /** Exporte les préférences d'un utilisateur */ + public Map exporterPreferences(UUID utilisateurId) { + LOG.infof("Export des préférences pour l'utilisateur %s", utilisateurId); + + Map export = new HashMap<>(); + export.put("utilisateurId", utilisateurId); + export.put("preferences", obtenirPreferences(utilisateurId)); + export.put("dateExport", java.time.LocalDateTime.now()); + + return export; + } + + /** Importe les préférences d'un utilisateur */ + @SuppressWarnings("unchecked") + public void importerPreferences(UUID utilisateurId, Map donnees) { + LOG.infof("Import des préférences pour l'utilisateur %s", utilisateurId); + + if (donnees.containsKey("preferences")) { + Map preferences = (Map) donnees.get("preferences"); + mettreAJourPreferences(utilisateurId, preferences); + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/PropositionAideService.java b/src/main/java/dev/lions/unionflow/server/service/PropositionAideService.java new file mode 100644 index 0000000..cb1ef43 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/PropositionAideService.java @@ -0,0 +1,442 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.solidarite.DemandeAideDTO; +import dev.lions.unionflow.server.api.dto.solidarite.PropositionAideDTO; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** + * Service spécialisé pour la gestion des propositions d'aide + * + *

Ce service gère le cycle de vie des propositions d'aide : création, activation, matching, + * suivi des performances. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-16 + */ +@ApplicationScoped +public class PropositionAideService { + + private static final Logger LOG = Logger.getLogger(PropositionAideService.class); + + // Cache pour les propositions actives + private final Map cachePropositionsActives = new HashMap<>(); + private final Map> indexParType = new HashMap<>(); + + // === OPÉRATIONS CRUD === + + /** + * Crée une nouvelle proposition d'aide + * + * @param propositionDTO La proposition à créer + * @return La proposition créée avec ID généré + */ + @Transactional + public PropositionAideDTO creerProposition(@Valid PropositionAideDTO propositionDTO) { + LOG.infof("Création d'une nouvelle proposition d'aide: %s", propositionDTO.getTitre()); + + // Génération des identifiants + propositionDTO.setId(UUID.randomUUID().toString()); + propositionDTO.setNumeroReference(genererNumeroReference()); + + // Initialisation des dates + LocalDateTime maintenant = LocalDateTime.now(); + propositionDTO.setDateCreation(maintenant); + propositionDTO.setDateModification(maintenant); + + // Statut initial + if (propositionDTO.getStatut() == null) { + propositionDTO.setStatut(PropositionAideDTO.StatutProposition.ACTIVE); + } + + // Calcul de la date d'expiration si non définie + if (propositionDTO.getDateExpiration() == null) { + propositionDTO.setDateExpiration(maintenant.plusMonths(6)); // 6 mois par défaut + } + + // Initialisation des compteurs + propositionDTO.setNombreDemandesTraitees(0); + propositionDTO.setNombreBeneficiairesAides(0); + propositionDTO.setMontantTotalVerse(0.0); + propositionDTO.setNombreVues(0); + propositionDTO.setNombreCandidatures(0); + propositionDTO.setNombreEvaluations(0); + + // Calcul du score de pertinence initial + propositionDTO.setScorePertinence(calculerScorePertinence(propositionDTO)); + + // Ajout au cache et index + ajouterAuCache(propositionDTO); + ajouterAIndex(propositionDTO); + + LOG.infof("Proposition d'aide créée avec succès: %s", propositionDTO.getId()); + return propositionDTO; + } + + /** + * Met à jour une proposition d'aide existante + * + * @param propositionDTO La proposition à mettre à jour + * @return La proposition mise à jour + */ + @Transactional + public PropositionAideDTO mettreAJour(@Valid PropositionAideDTO propositionDTO) { + LOG.infof("Mise à jour de la proposition d'aide: %s", propositionDTO.getId()); + + // Mise à jour de la date de modification + propositionDTO.setDateModification(LocalDateTime.now()); + + // Recalcul du score de pertinence + propositionDTO.setScorePertinence(calculerScorePertinence(propositionDTO)); + + // Mise à jour du cache et index + ajouterAuCache(propositionDTO); + mettreAJourIndex(propositionDTO); + + LOG.infof("Proposition d'aide mise à jour avec succès: %s", propositionDTO.getId()); + return propositionDTO; + } + + /** + * Obtient une proposition d'aide par son ID + * + * @param id ID de la proposition + * @return La proposition trouvée + */ + public PropositionAideDTO obtenirParId(@NotBlank String id) { + LOG.debugf("Récupération de la proposition d'aide: %s", id); + + // Vérification du cache + PropositionAideDTO propositionCachee = cachePropositionsActives.get(id); + if (propositionCachee != null) { + // Incrémenter le nombre de vues + propositionCachee.setNombreVues(propositionCachee.getNombreVues() + 1); + return propositionCachee; + } + + // Simulation de récupération depuis la base de données + PropositionAideDTO proposition = simulerRecuperationBDD(id); + + if (proposition != null) { + ajouterAuCache(proposition); + ajouterAIndex(proposition); + } + + return proposition; + } + + /** + * Active ou désactive une proposition d'aide + * + * @param propositionId ID de la proposition + * @param activer true pour activer, false pour désactiver + * @return La proposition mise à jour + */ + @Transactional + public PropositionAideDTO changerStatutActivation( + @NotBlank String propositionId, boolean activer) { + LOG.infof( + "Changement de statut d'activation pour la proposition %s: %s", + propositionId, activer ? "ACTIVE" : "SUSPENDUE"); + + PropositionAideDTO proposition = obtenirParId(propositionId); + if (proposition == null) { + throw new IllegalArgumentException("Proposition non trouvée: " + propositionId); + } + + if (activer) { + // Vérifications avant activation + if (proposition.isExpiree()) { + throw new IllegalStateException("Impossible d'activer une proposition expirée"); + } + proposition.setStatut(PropositionAideDTO.StatutProposition.ACTIVE); + proposition.setEstDisponible(true); + } else { + proposition.setStatut(PropositionAideDTO.StatutProposition.SUSPENDUE); + proposition.setEstDisponible(false); + } + + proposition.setDateModification(LocalDateTime.now()); + + // Mise à jour du cache et index + ajouterAuCache(proposition); + mettreAJourIndex(proposition); + + return proposition; + } + + // === RECHERCHE ET MATCHING === + + /** + * Recherche des propositions compatibles avec une demande + * + * @param demande La demande d'aide + * @return Liste des propositions compatibles triées par score + */ + public List rechercherPropositionsCompatibles(DemandeAideDTO demande) { + LOG.debugf("Recherche de propositions compatibles pour la demande: %s", demande.getId()); + + // Recherche par type d'aide d'abord + List candidats = + indexParType.getOrDefault(demande.getTypeAide(), new ArrayList<>()); + + // Si pas de correspondance exacte, chercher dans la même catégorie + if (candidats.isEmpty()) { + candidats = + cachePropositionsActives.values().stream() + .filter( + p -> p.getTypeAide().getCategorie().equals(demande.getTypeAide().getCategorie())) + .collect(Collectors.toList()); + } + + // Filtrage et scoring + return candidats.stream() + .filter(PropositionAideDTO::isActiveEtDisponible) + .filter(p -> p.peutAccepterBeneficiaires()) + .map( + p -> { + double score = p.getScoreCompatibilite(demande); + // Stocker le score temporairement dans les données personnalisées + if (p.getDonneesPersonnalisees() == null) { + p.setDonneesPersonnalisees(new HashMap<>()); + } + p.getDonneesPersonnalisees().put("scoreCompatibilite", score); + return p; + }) + .filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreCompatibilite") >= 30.0) + .sorted( + (p1, p2) -> { + Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreCompatibilite"); + Double score2 = (Double) p2.getDonneesPersonnalisees().get("scoreCompatibilite"); + return Double.compare(score2, score1); // Ordre décroissant + }) + .limit(10) // Limiter à 10 meilleures propositions + .collect(Collectors.toList()); + } + + /** + * Recherche des propositions par critères + * + * @param filtres Map des critères de recherche + * @return Liste des propositions correspondantes + */ + public List rechercherAvecFiltres(Map filtres) { + LOG.debugf("Recherche de propositions avec filtres: %s", filtres); + + return cachePropositionsActives.values().stream() + .filter(proposition -> correspondAuxFiltres(proposition, filtres)) + .sorted(this::comparerParPertinence) + .collect(Collectors.toList()); + } + + /** + * Obtient les propositions actives pour un type d'aide + * + * @param typeAide Type d'aide recherché + * @return Liste des propositions actives + */ + public List obtenirPropositionsActives(TypeAide typeAide) { + LOG.debugf("Récupération des propositions actives pour le type: %s", typeAide); + + return indexParType.getOrDefault(typeAide, new ArrayList<>()).stream() + .filter(PropositionAideDTO::isActiveEtDisponible) + .sorted(this::comparerParPertinence) + .collect(Collectors.toList()); + } + + /** + * Obtient les meilleures propositions (top performers) + * + * @param limite Nombre maximum de propositions à retourner + * @return Liste des meilleures propositions + */ + public List obtenirMeilleuresPropositions(int limite) { + LOG.debugf("Récupération des %d meilleures propositions", limite); + + return cachePropositionsActives.values().stream() + .filter(PropositionAideDTO::isActiveEtDisponible) + .filter(p -> p.getNombreEvaluations() >= 3) // Au moins 3 évaluations + .filter(p -> p.getNoteMoyenne() != null && p.getNoteMoyenne() >= 4.0) + .sorted( + (p1, p2) -> { + // Tri par note moyenne puis par nombre d'aides réalisées + int compareNote = Double.compare(p2.getNoteMoyenne(), p1.getNoteMoyenne()); + if (compareNote != 0) return compareNote; + return Integer.compare( + p2.getNombreBeneficiairesAides(), p1.getNombreBeneficiairesAides()); + }) + .limit(limite) + .collect(Collectors.toList()); + } + + // === GESTION DES PERFORMANCES === + + /** + * Met à jour les statistiques d'une proposition après une aide fournie + * + * @param propositionId ID de la proposition + * @param montantVerse Montant versé (si applicable) + * @param nombreBeneficiaires Nombre de bénéficiaires aidés + * @return La proposition mise à jour + */ + @Transactional + public PropositionAideDTO mettreAJourStatistiques( + @NotBlank String propositionId, Double montantVerse, int nombreBeneficiaires) { + LOG.infof("Mise à jour des statistiques pour la proposition: %s", propositionId); + + PropositionAideDTO proposition = obtenirParId(propositionId); + if (proposition == null) { + throw new IllegalArgumentException("Proposition non trouvée: " + propositionId); + } + + // Mise à jour des compteurs + proposition.setNombreDemandesTraitees(proposition.getNombreDemandesTraitees() + 1); + proposition.setNombreBeneficiairesAides( + proposition.getNombreBeneficiairesAides() + nombreBeneficiaires); + + if (montantVerse != null) { + proposition.setMontantTotalVerse(proposition.getMontantTotalVerse() + montantVerse); + } + + // Recalcul du score de pertinence + proposition.setScorePertinence(calculerScorePertinence(proposition)); + + // Vérification si la capacité maximale est atteinte + if (proposition.getNombreBeneficiairesAides() >= proposition.getNombreMaxBeneficiaires()) { + proposition.setEstDisponible(false); + proposition.setStatut(PropositionAideDTO.StatutProposition.TERMINEE); + } + + proposition.setDateModification(LocalDateTime.now()); + + // Mise à jour du cache + ajouterAuCache(proposition); + + return proposition; + } + + // === MÉTHODES UTILITAIRES PRIVÉES === + + /** Génère un numéro de référence unique pour les propositions */ + private String genererNumeroReference() { + int annee = LocalDateTime.now().getYear(); + int numero = (int) (Math.random() * 999999) + 1; + return String.format("PA-%04d-%06d", annee, numero); + } + + /** Calcule le score de pertinence d'une proposition */ + private double calculerScorePertinence(PropositionAideDTO proposition) { + double score = 50.0; // Score de base + + // Bonus pour l'expérience (nombre d'aides réalisées) + score += Math.min(20.0, proposition.getNombreBeneficiairesAides() * 2.0); + + // Bonus pour la note moyenne + if (proposition.getNoteMoyenne() != null) { + score += (proposition.getNoteMoyenne() - 3.0) * 10.0; // +10 par point au-dessus de 3 + } + + // Bonus pour la récence + long joursDepuisCreation = + java.time.Duration.between(proposition.getDateCreation(), LocalDateTime.now()).toDays(); + if (joursDepuisCreation <= 30) { + score += 10.0; + } else if (joursDepuisCreation <= 90) { + score += 5.0; + } + + // Bonus pour la disponibilité + if (proposition.isActiveEtDisponible()) { + score += 15.0; + } + + // Malus pour l'inactivité + if (proposition.getNombreVues() == 0) { + score -= 10.0; + } + + return Math.max(0.0, Math.min(100.0, score)); + } + + /** Vérifie si une proposition correspond aux filtres */ + private boolean correspondAuxFiltres( + PropositionAideDTO proposition, Map filtres) { + for (Map.Entry filtre : filtres.entrySet()) { + String cle = filtre.getKey(); + Object valeur = filtre.getValue(); + + switch (cle) { + case "typeAide" -> { + if (!proposition.getTypeAide().equals(valeur)) return false; + } + case "statut" -> { + if (!proposition.getStatut().equals(valeur)) return false; + } + case "proposantId" -> { + if (!proposition.getProposantId().equals(valeur)) return false; + } + case "organisationId" -> { + if (!proposition.getOrganisationId().equals(valeur)) return false; + } + case "estDisponible" -> { + if (!proposition.getEstDisponible().equals(valeur)) return false; + } + case "montantMaximum" -> { + if (proposition.getMontantMaximum() == null + || proposition.getMontantMaximum().compareTo(BigDecimal.valueOf((Double) valeur)) < 0) return false; + } + } + } + return true; + } + + /** Compare deux propositions par pertinence */ + private int comparerParPertinence(PropositionAideDTO p1, PropositionAideDTO p2) { + // D'abord par score de pertinence (plus haut = meilleur) + int compareScore = Double.compare(p2.getScorePertinence(), p1.getScorePertinence()); + if (compareScore != 0) return compareScore; + + // Puis par date de création (plus récent = meilleur) + return p2.getDateCreation().compareTo(p1.getDateCreation()); + } + + // === GESTION DU CACHE ET INDEX === + + private void ajouterAuCache(PropositionAideDTO proposition) { + cachePropositionsActives.put(proposition.getId(), proposition); + } + + private void ajouterAIndex(PropositionAideDTO proposition) { + indexParType + .computeIfAbsent(proposition.getTypeAide(), k -> new ArrayList<>()) + .add(proposition); + } + + private void mettreAJourIndex(PropositionAideDTO proposition) { + // Supprimer de tous les index + indexParType + .values() + .forEach(liste -> liste.removeIf(p -> p.getId().equals(proposition.getId()))); + + // Ré-ajouter si la proposition est active + if (proposition.isActiveEtDisponible()) { + ajouterAIndex(proposition); + } + } + + // === MÉTHODES DE SIMULATION (À REMPLACER PAR DE VRAIS REPOSITORIES) === + + private PropositionAideDTO simulerRecuperationBDD(String id) { + // Simulation - dans une vraie implémentation, ceci ferait appel au repository + return null; + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/RoleService.java b/src/main/java/dev/lions/unionflow/server/service/RoleService.java new file mode 100644 index 0000000..35c57a8 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/RoleService.java @@ -0,0 +1,171 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.entity.Role; +import dev.lions.unionflow.server.entity.Role.TypeRole; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import dev.lions.unionflow.server.repository.RoleRepository; +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import java.util.List; +import java.util.UUID; +import org.jboss.logging.Logger; + +/** + * Service métier pour la gestion des rôles + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class RoleService { + + private static final Logger LOG = Logger.getLogger(RoleService.class); + + @Inject RoleRepository roleRepository; + + @Inject OrganisationRepository organisationRepository; + + @Inject KeycloakService keycloakService; + + /** + * Crée un nouveau rôle + * + * @param role Rôle à créer + * @return Rôle créé + */ + @Transactional + public Role creerRole(Role role) { + LOG.infof("Création d'un nouveau rôle: %s", role.getCode()); + + // Vérifier l'unicité du code + if (roleRepository.findByCode(role.getCode()).isPresent()) { + throw new IllegalArgumentException("Un rôle avec ce code existe déjà: " + role.getCode()); + } + + // Métadonnées + role.setCreePar(keycloakService.getCurrentUserEmail()); + + roleRepository.persist(role); + LOG.infof("Rôle créé avec succès: ID=%s, Code=%s", role.getId(), role.getCode()); + + return role; + } + + /** + * Met à jour un rôle existant + * + * @param id ID du rôle + * @param roleModifie Rôle avec les modifications + * @return Rôle mis à jour + */ + @Transactional + public Role mettreAJourRole(UUID id, Role roleModifie) { + LOG.infof("Mise à jour du rôle ID: %s", id); + + Role role = + roleRepository + .findRoleById(id) + .orElseThrow(() -> new NotFoundException("Rôle non trouvé avec l'ID: " + id)); + + // Vérifier l'unicité du code si modifié + if (!role.getCode().equals(roleModifie.getCode())) { + if (roleRepository.findByCode(roleModifie.getCode()).isPresent()) { + throw new IllegalArgumentException("Un rôle avec ce code existe déjà: " + roleModifie.getCode()); + } + } + + // Mise à jour + role.setCode(roleModifie.getCode()); + role.setLibelle(roleModifie.getLibelle()); + role.setDescription(roleModifie.getDescription()); + role.setNiveauHierarchique(roleModifie.getNiveauHierarchique()); + role.setTypeRole(roleModifie.getTypeRole()); + role.setOrganisation(roleModifie.getOrganisation()); + role.setModifiePar(keycloakService.getCurrentUserEmail()); + + roleRepository.persist(role); + LOG.infof("Rôle mis à jour avec succès: ID=%s", id); + + return role; + } + + /** + * Trouve un rôle par son ID + * + * @param id ID du rôle + * @return Rôle ou null + */ + public Role trouverParId(UUID id) { + return roleRepository.findRoleById(id).orElse(null); + } + + /** + * Trouve un rôle par son code + * + * @param code Code du rôle + * @return Rôle ou null + */ + public Role trouverParCode(String code) { + return roleRepository.findByCode(code).orElse(null); + } + + /** + * Liste tous les rôles système + * + * @return Liste des rôles système + */ + public List listerRolesSysteme() { + return roleRepository.findRolesSysteme(); + } + + /** + * Liste tous les rôles d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des rôles + */ + public List listerParOrganisation(UUID organisationId) { + return roleRepository.findByOrganisationId(organisationId); + } + + /** + * Liste tous les rôles actifs + * + * @return Liste des rôles actifs + */ + public List listerTousActifs() { + return roleRepository.findAllActifs(); + } + + /** + * Supprime (désactive) un rôle + * + * @param id ID du rôle + */ + @Transactional + public void supprimerRole(UUID id) { + LOG.infof("Suppression du rôle ID: %s", id); + + Role role = + roleRepository + .findRoleById(id) + .orElseThrow(() -> new NotFoundException("Rôle non trouvé avec l'ID: " + id)); + + // Vérifier si c'est un rôle système + if (role.isRoleSysteme()) { + throw new IllegalStateException("Impossible de supprimer un rôle système"); + } + + role.setActif(false); + role.setModifiePar(keycloakService.getCurrentUserEmail()); + + roleRepository.persist(role); + LOG.infof("Rôle supprimé avec succès: ID=%s", id); + } +} + diff --git a/src/main/java/dev/lions/unionflow/server/service/TrendAnalysisService.java b/src/main/java/dev/lions/unionflow/server/service/TrendAnalysisService.java new file mode 100644 index 0000000..a2fbfe9 --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/TrendAnalysisService.java @@ -0,0 +1,412 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.analytics.KPITrendDTO; +import dev.lions.unionflow.server.api.enums.analytics.PeriodeAnalyse; +import dev.lions.unionflow.server.api.enums.analytics.TypeMetrique; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; + +/** + * Service d'analyse des tendances et prédictions pour les KPI + * + *

Ce service calcule les tendances, effectue des analyses statistiques et génère des prédictions + * basées sur l'historique des données. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-16 + */ +@ApplicationScoped +@Slf4j +public class TrendAnalysisService { + + @Inject AnalyticsService analyticsService; + + @Inject KPICalculatorService kpiCalculatorService; + + /** + * Calcule la tendance d'un KPI sur une période donnée + * + * @param typeMetrique Le type de métrique à analyser + * @param periodeAnalyse La période d'analyse + * @param organisationId L'ID de l'organisation (optionnel) + * @return Les données de tendance du KPI + */ + public KPITrendDTO calculerTendance( + TypeMetrique typeMetrique, PeriodeAnalyse periodeAnalyse, UUID organisationId) { + log.info( + "Calcul de la tendance pour {} sur la période {} et l'organisation {}", + typeMetrique, + periodeAnalyse, + organisationId); + + LocalDateTime dateDebut = periodeAnalyse.getDateDebut(); + LocalDateTime dateFin = periodeAnalyse.getDateFin(); + + // Génération des points de données historiques + List pointsDonnees = + genererPointsDonnees(typeMetrique, dateDebut, dateFin, organisationId); + + // Calculs statistiques + StatistiquesDTO stats = calculerStatistiques(pointsDonnees); + + // Analyse de tendance (régression linéaire simple) + TendanceDTO tendance = calculerTendanceLineaire(pointsDonnees); + + // Prédiction pour la prochaine période + BigDecimal prediction = calculerPrediction(pointsDonnees, tendance); + + // Détection d'anomalies + detecterAnomalies(pointsDonnees, stats); + + return KPITrendDTO.builder() + .typeMetrique(typeMetrique) + .periodeAnalyse(periodeAnalyse) + .organisationId(organisationId) + .nomOrganisation(obtenirNomOrganisation(organisationId)) + .dateDebut(dateDebut) + .dateFin(dateFin) + .pointsDonnees(pointsDonnees) + .valeurActuelle(stats.valeurActuelle) + .valeurMinimale(stats.valeurMinimale) + .valeurMaximale(stats.valeurMaximale) + .valeurMoyenne(stats.valeurMoyenne) + .ecartType(stats.ecartType) + .coefficientVariation(stats.coefficientVariation) + .tendanceGenerale(tendance.pente) + .coefficientCorrelation(tendance.coefficientCorrelation) + .pourcentageEvolutionGlobale(calculerEvolutionGlobale(pointsDonnees)) + .predictionProchainePeriode(prediction) + .margeErreurPrediction(calculerMargeErreur(tendance)) + .seuilAlerteBas(calculerSeuilAlerteBas(stats)) + .seuilAlerteHaut(calculerSeuilAlerteHaut(stats)) + .alerteActive(verifierAlertes(stats.valeurActuelle, stats)) + .intervalleRegroupement(periodeAnalyse.getIntervalleRegroupement()) + .formatDate(periodeAnalyse.getFormatDate()) + .dateDerniereMiseAJour(LocalDateTime.now()) + .frequenceMiseAJourMinutes(determinerFrequenceMiseAJour(periodeAnalyse)) + .build(); + } + + /** Génère les points de données historiques pour la période */ + private List genererPointsDonnees( + TypeMetrique typeMetrique, + LocalDateTime dateDebut, + LocalDateTime dateFin, + UUID organisationId) { + List points = new ArrayList<>(); + + // Déterminer l'intervalle entre les points + ChronoUnit unite = determinerUniteIntervalle(dateDebut, dateFin); + long intervalleValeur = determinerValeurIntervalle(dateDebut, dateFin, unite); + + LocalDateTime dateCourante = dateDebut; + int index = 0; + + while (!dateCourante.isAfter(dateFin)) { + LocalDateTime dateFinIntervalle = dateCourante.plus(intervalleValeur, unite); + if (dateFinIntervalle.isAfter(dateFin)) { + dateFinIntervalle = dateFin; + } + + // Calcul de la valeur pour cet intervalle + BigDecimal valeur = + calculerValeurPourIntervalle( + typeMetrique, dateCourante, dateFinIntervalle, organisationId); + + KPITrendDTO.PointDonneeDTO point = + KPITrendDTO.PointDonneeDTO.builder() + .date(dateCourante) + .valeur(valeur) + .libelle(formaterLibellePoint(dateCourante, unite)) + .anomalie(false) // Sera déterminé plus tard + .prediction(false) + .build(); + + points.add(point); + dateCourante = dateCourante.plus(intervalleValeur, unite); + index++; + } + + log.info("Généré {} points de données pour la tendance", points.size()); + return points; + } + + /** Calcule les statistiques descriptives des points de données */ + private StatistiquesDTO calculerStatistiques(List points) { + if (points.isEmpty()) { + return new StatistiquesDTO(); + } + + List valeurs = points.stream().map(KPITrendDTO.PointDonneeDTO::getValeur).toList(); + + BigDecimal valeurActuelle = points.get(points.size() - 1).getValeur(); + BigDecimal valeurMinimale = valeurs.stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO); + BigDecimal valeurMaximale = valeurs.stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO); + + // Calcul de la moyenne + BigDecimal somme = valeurs.stream().reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal moyenne = somme.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP); + + // Calcul de l'écart-type + BigDecimal sommeDifferencesCarrees = + valeurs.stream() + .map(v -> v.subtract(moyenne).pow(2)) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal variance = + sommeDifferencesCarrees.divide(new BigDecimal(valeurs.size()), 4, RoundingMode.HALF_UP); + BigDecimal ecartType = + new BigDecimal(Math.sqrt(variance.doubleValue())).setScale(4, RoundingMode.HALF_UP); + + // Coefficient de variation + BigDecimal coefficientVariation = + moyenne.compareTo(BigDecimal.ZERO) != 0 + ? ecartType.divide(moyenne, 4, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + + return new StatistiquesDTO( + valeurActuelle, valeurMinimale, valeurMaximale, moyenne, ecartType, coefficientVariation); + } + + /** Calcule la tendance linéaire (régression linéaire simple) */ + private TendanceDTO calculerTendanceLineaire(List points) { + if (points.size() < 2) { + return new TendanceDTO(BigDecimal.ZERO, BigDecimal.ZERO); + } + + int n = points.size(); + BigDecimal sommeX = BigDecimal.ZERO; + BigDecimal sommeY = BigDecimal.ZERO; + BigDecimal sommeXY = BigDecimal.ZERO; + BigDecimal sommeX2 = BigDecimal.ZERO; + BigDecimal sommeY2 = BigDecimal.ZERO; + + for (int i = 0; i < n; i++) { + BigDecimal x = new BigDecimal(i); // Index comme variable X + BigDecimal y = points.get(i).getValeur(); // Valeur comme variable Y + + sommeX = sommeX.add(x); + sommeY = sommeY.add(y); + sommeXY = sommeXY.add(x.multiply(y)); + sommeX2 = sommeX2.add(x.multiply(x)); + sommeY2 = sommeY2.add(y.multiply(y)); + } + + // Calcul de la pente (coefficient directeur) + BigDecimal nBD = new BigDecimal(n); + BigDecimal numerateur = nBD.multiply(sommeXY).subtract(sommeX.multiply(sommeY)); + BigDecimal denominateur = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX)); + + BigDecimal pente = + denominateur.compareTo(BigDecimal.ZERO) != 0 + ? numerateur.divide(denominateur, 6, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + + // Calcul du coefficient de corrélation R² + BigDecimal numerateurR = numerateur; + BigDecimal denominateurR1 = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX)); + BigDecimal denominateurR2 = nBD.multiply(sommeY2).subtract(sommeY.multiply(sommeY)); + + BigDecimal coefficientCorrelation = BigDecimal.ZERO; + if (denominateurR1.compareTo(BigDecimal.ZERO) != 0 + && denominateurR2.compareTo(BigDecimal.ZERO) != 0) { + BigDecimal denominateurR = + new BigDecimal(Math.sqrt(denominateurR1.multiply(denominateurR2).doubleValue())); + + if (denominateurR.compareTo(BigDecimal.ZERO) != 0) { + BigDecimal r = numerateurR.divide(denominateurR, 6, RoundingMode.HALF_UP); + coefficientCorrelation = r.multiply(r); // R² + } + } + + return new TendanceDTO(pente, coefficientCorrelation); + } + + /** Calcule une prédiction pour la prochaine période */ + private BigDecimal calculerPrediction( + List points, TendanceDTO tendance) { + if (points.isEmpty()) return BigDecimal.ZERO; + + BigDecimal derniereValeur = points.get(points.size() - 1).getValeur(); + BigDecimal prediction = derniereValeur.add(tendance.pente); + + // S'assurer que la prédiction est positive + return prediction.max(BigDecimal.ZERO); + } + + /** Détecte les anomalies dans les points de données */ + private void detecterAnomalies(List points, StatistiquesDTO stats) { + BigDecimal seuilAnomalie = stats.ecartType.multiply(new BigDecimal("2")); // 2 écarts-types + + for (KPITrendDTO.PointDonneeDTO point : points) { + BigDecimal ecartMoyenne = point.getValeur().subtract(stats.valeurMoyenne).abs(); + if (ecartMoyenne.compareTo(seuilAnomalie) > 0) { + point.setAnomalie(true); + } + } + } + + // === MÉTHODES UTILITAIRES === + + private ChronoUnit determinerUniteIntervalle(LocalDateTime dateDebut, LocalDateTime dateFin) { + long joursTotal = ChronoUnit.DAYS.between(dateDebut, dateFin); + + if (joursTotal <= 7) return ChronoUnit.DAYS; + if (joursTotal <= 90) return ChronoUnit.DAYS; + if (joursTotal <= 365) return ChronoUnit.WEEKS; + return ChronoUnit.MONTHS; + } + + private long determinerValeurIntervalle( + LocalDateTime dateDebut, LocalDateTime dateFin, ChronoUnit unite) { + long dureeTotal = unite.between(dateDebut, dateFin); + + // Viser environ 10-20 points de données + if (dureeTotal <= 20) return 1; + if (dureeTotal <= 40) return 2; + if (dureeTotal <= 100) return 5; + return dureeTotal / 15; // Environ 15 points + } + + private BigDecimal calculerValeurPourIntervalle( + TypeMetrique typeMetrique, + LocalDateTime dateDebut, + LocalDateTime dateFin, + UUID organisationId) { + // Utiliser le service KPI pour calculer la valeur + return switch (typeMetrique) { + case NOMBRE_MEMBRES_ACTIFS -> { + // Calcul direct via le service KPI + var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin); + yield kpis.getOrDefault(TypeMetrique.NOMBRE_MEMBRES_ACTIFS, BigDecimal.ZERO); + } + case TOTAL_COTISATIONS_COLLECTEES -> { + var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin); + yield kpis.getOrDefault(TypeMetrique.TOTAL_COTISATIONS_COLLECTEES, BigDecimal.ZERO); + } + case NOMBRE_EVENEMENTS_ORGANISES -> { + var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin); + yield kpis.getOrDefault(TypeMetrique.NOMBRE_EVENEMENTS_ORGANISES, BigDecimal.ZERO); + } + case NOMBRE_DEMANDES_AIDE -> { + var kpis = kpiCalculatorService.calculerTousLesKPI(organisationId, dateDebut, dateFin); + yield kpis.getOrDefault(TypeMetrique.NOMBRE_DEMANDES_AIDE, BigDecimal.ZERO); + } + default -> BigDecimal.ZERO; + }; + } + + private String formaterLibellePoint(LocalDateTime date, ChronoUnit unite) { + return switch (unite) { + case DAYS -> date.toLocalDate().toString(); + case WEEKS -> "S" + date.get(java.time.temporal.WeekFields.ISO.weekOfYear()); + case MONTHS -> date.getMonth().toString() + " " + date.getYear(); + default -> date.toString(); + }; + } + + private BigDecimal calculerEvolutionGlobale(List points) { + if (points.size() < 2) return BigDecimal.ZERO; + + BigDecimal premiereValeur = points.get(0).getValeur(); + BigDecimal derniereValeur = points.get(points.size() - 1).getValeur(); + + if (premiereValeur.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO; + + return derniereValeur + .subtract(premiereValeur) + .divide(premiereValeur, 4, RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + } + + private BigDecimal calculerMargeErreur(TendanceDTO tendance) { + // Marge d'erreur basée sur le coefficient de corrélation + BigDecimal precision = tendance.coefficientCorrelation; + BigDecimal margeErreur = BigDecimal.ONE.subtract(precision).multiply(new BigDecimal("100")); + return margeErreur.min(new BigDecimal("50")); // Plafonnée à 50% + } + + private BigDecimal calculerSeuilAlerteBas(StatistiquesDTO stats) { + return stats.valeurMoyenne.subtract(stats.ecartType.multiply(new BigDecimal("1.5"))); + } + + private BigDecimal calculerSeuilAlerteHaut(StatistiquesDTO stats) { + return stats.valeurMoyenne.add(stats.ecartType.multiply(new BigDecimal("1.5"))); + } + + private Boolean verifierAlertes(BigDecimal valeurActuelle, StatistiquesDTO stats) { + BigDecimal seuilBas = calculerSeuilAlerteBas(stats); + BigDecimal seuilHaut = calculerSeuilAlerteHaut(stats); + + return valeurActuelle.compareTo(seuilBas) < 0 || valeurActuelle.compareTo(seuilHaut) > 0; + } + + private Integer determinerFrequenceMiseAJour(PeriodeAnalyse periode) { + return switch (periode) { + case AUJOURD_HUI, HIER -> 15; // 15 minutes + case CETTE_SEMAINE, SEMAINE_DERNIERE -> 60; // 1 heure + case CE_MOIS, MOIS_DERNIER -> 240; // 4 heures + default -> 1440; // 24 heures + }; + } + + private String obtenirNomOrganisation(UUID organisationId) { + // À implémenter avec le repository + return null; + } + + // === CLASSES INTERNES === + + private static class StatistiquesDTO { + final BigDecimal valeurActuelle; + final BigDecimal valeurMinimale; + final BigDecimal valeurMaximale; + final BigDecimal valeurMoyenne; + final BigDecimal ecartType; + final BigDecimal coefficientVariation; + + StatistiquesDTO() { + this( + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO, + BigDecimal.ZERO); + } + + StatistiquesDTO( + BigDecimal valeurActuelle, + BigDecimal valeurMinimale, + BigDecimal valeurMaximale, + BigDecimal valeurMoyenne, + BigDecimal ecartType, + BigDecimal coefficientVariation) { + this.valeurActuelle = valeurActuelle; + this.valeurMinimale = valeurMinimale; + this.valeurMaximale = valeurMaximale; + this.valeurMoyenne = valeurMoyenne; + this.ecartType = ecartType; + this.coefficientVariation = coefficientVariation; + } + } + + private static class TendanceDTO { + final BigDecimal pente; + final BigDecimal coefficientCorrelation; + + TendanceDTO(BigDecimal pente, BigDecimal coefficientCorrelation) { + this.pente = pente; + this.coefficientCorrelation = coefficientCorrelation; + } + } +} diff --git a/src/main/java/dev/lions/unionflow/server/service/TypeOrganisationService.java b/src/main/java/dev/lions/unionflow/server/service/TypeOrganisationService.java new file mode 100644 index 0000000..3e3518a --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/TypeOrganisationService.java @@ -0,0 +1,146 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.organisation.TypeOrganisationDTO; +import dev.lions.unionflow.server.entity.TypeOrganisationEntity; +import dev.lions.unionflow.server.repository.TypeOrganisationRepository; +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + + /** + * Service de gestion du catalogue des types d'organisation. + * + *

Synchronise les types persistés avec l'enum {@link TypeOrganisation} pour les valeurs + * par défaut, puis permet un CRUD entièrement dynamique (les nouveaux codes ne sont plus + * limités aux valeurs de l'enum). + */ +@ApplicationScoped +public class TypeOrganisationService { + + private static final Logger LOG = Logger.getLogger(TypeOrganisationService.class); + + @Inject TypeOrganisationRepository repository; + @Inject KeycloakService keycloakService; + + // Plus d'initialisation automatique : le catalogue des types est désormais entièrement + // géré en mode CRUD via l'UI d'administration. Aucune donnée fictive n'est injectée + // au démarrage ; si nécessaire, utilisez des scripts de migration (Flyway) ou l'UI. + + /** Retourne la liste de tous les types (optionnellement seulement actifs). */ + public List listAll(boolean onlyActifs) { + List entities = + onlyActifs ? repository.listActifsOrdennes() : repository.listAll(); + return entities.stream().map(this::toDTO).collect(Collectors.toList()); + } + + /** Crée un nouveau type. Le code doit être non vide et unique. */ + @Transactional + public TypeOrganisationDTO create(TypeOrganisationDTO dto) { + validateCode(dto.getCode()); + + // Si un type existe déjà pour ce code, on retourne simplement l'existant + // (comportement idempotent côté API) plutôt que de remonter une 400. + // Le CRUD complet reste possible via l'écran d'édition. + var existingOpt = repository.findByCode(dto.getCode()); + if (existingOpt.isPresent()) { + LOG.infof( + "Type d'organisation déjà existant pour le code %s, retour de l'entrée existante.", + dto.getCode()); + return toDTO(existingOpt.get()); + } + + TypeOrganisationEntity entity = new TypeOrganisationEntity(); + // métadonnées de création + entity.setCreePar(keycloakService.getCurrentUserEmail()); + applyToEntity(dto, entity); + repository.persist(entity); + return toDTO(entity); + } + + /** Met à jour un type existant. L'ID est utilisé comme identifiant principal. */ + @Transactional + public TypeOrganisationDTO update(UUID id, TypeOrganisationDTO dto) { + TypeOrganisationEntity entity = + repository + .findByIdOptional(id) + .orElseThrow(() -> new IllegalArgumentException("Type d'organisation introuvable")); + + if (dto.getCode() != null && !dto.getCode().equalsIgnoreCase(entity.getCode())) { + validateCode(dto.getCode()); + repository + .findByCode(dto.getCode()) + .ifPresent( + existing -> { + if (!existing.getId().equals(id)) { + throw new IllegalArgumentException( + "Un autre type d'organisation utilise déjà le code: " + dto.getCode()); + } + }); + entity.setCode(dto.getCode()); + } + + // métadonnées de modification + entity.setModifiePar(keycloakService.getCurrentUserEmail()); + applyToEntity(dto, entity); + repository.update(entity); + return toDTO(entity); + } + + /** Désactive logiquement un type. */ + @Transactional + public void disable(UUID id) { + TypeOrganisationEntity entity = + repository + .findByIdOptional(id) + .orElseThrow(() -> new IllegalArgumentException("Type d'organisation introuvable")); + entity.setActif(false); + repository.update(entity); + } + + private void validateCode(String code) { + if (code == null || code.trim().isEmpty()) { + throw new IllegalArgumentException("Le code du type d'organisation est obligatoire"); + } + // Plus aucune contrainte de format technique côté backend pour éviter les 400 inutiles. + // Le code est simplement normalisé en majuscules dans applyToEntity, ce qui suffit + // pour garantir la cohérence métier et la clé fonctionnelle. + } + + private TypeOrganisationDTO toDTO(TypeOrganisationEntity entity) { + TypeOrganisationDTO dto = new TypeOrganisationDTO(); + dto.setId(entity.getId()); + dto.setDateCreation(entity.getDateCreation()); + dto.setDateModification(entity.getDateModification()); + dto.setActif(entity.getActif()); + dto.setVersion(entity.getVersion()); + + dto.setCode(entity.getCode()); + dto.setLibelle(entity.getLibelle()); + dto.setDescription(entity.getDescription()); + dto.setOrdreAffichage(entity.getOrdreAffichage()); + return dto; + } + + private void applyToEntity(TypeOrganisationDTO dto, TypeOrganisationEntity entity) { + if (dto.getCode() != null) { + entity.setCode(dto.getCode().toUpperCase()); + } + if (dto.getLibelle() != null) { + entity.setLibelle(dto.getLibelle()); + } + entity.setDescription(dto.getDescription()); + entity.setOrdreAffichage(dto.getOrdreAffichage()); + if (dto.getActif() != null) { + entity.setActif(dto.getActif()); + } + } +} + + diff --git a/src/main/java/dev/lions/unionflow/server/service/WaveService.java b/src/main/java/dev/lions/unionflow/server/service/WaveService.java new file mode 100644 index 0000000..d1db3da --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/service/WaveService.java @@ -0,0 +1,393 @@ +package dev.lions.unionflow.server.service; + +import dev.lions.unionflow.server.api.dto.wave.CompteWaveDTO; +import dev.lions.unionflow.server.api.dto.wave.TransactionWaveDTO; +import dev.lions.unionflow.server.api.enums.wave.StatutCompteWave; +import dev.lions.unionflow.server.api.enums.wave.StatutTransactionWave; +import dev.lions.unionflow.server.api.enums.wave.TypeTransactionWave; +import dev.lions.unionflow.server.entity.CompteWave; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.TransactionWave; +import dev.lions.unionflow.server.repository.CompteWaveRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.TransactionWaveRepository; +import dev.lions.unionflow.server.service.KeycloakService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +/** + * Service métier pour l'intégration Wave Mobile Money + * + * @author UnionFlow Team + * @version 3.0 + * @since 2025-01-29 + */ +@ApplicationScoped +public class WaveService { + + private static final Logger LOG = Logger.getLogger(WaveService.class); + + @Inject CompteWaveRepository compteWaveRepository; + + @Inject TransactionWaveRepository transactionWaveRepository; + + @Inject OrganisationRepository organisationRepository; + + @Inject MembreRepository membreRepository; + + @Inject KeycloakService keycloakService; + + /** + * Crée un nouveau compte Wave + * + * @param compteWaveDTO DTO du compte à créer + * @return DTO du compte créé + */ + @Transactional + public CompteWaveDTO creerCompteWave(CompteWaveDTO compteWaveDTO) { + LOG.infof("Création d'un nouveau compte Wave: %s", compteWaveDTO.getNumeroTelephone()); + + // Vérifier l'unicité du numéro de téléphone + if (compteWaveRepository.findByNumeroTelephone(compteWaveDTO.getNumeroTelephone()).isPresent()) { + throw new IllegalArgumentException( + "Un compte Wave existe déjà pour ce numéro: " + compteWaveDTO.getNumeroTelephone()); + } + + CompteWave compteWave = convertToEntity(compteWaveDTO); + compteWave.setCreePar(keycloakService.getCurrentUserEmail()); + + compteWaveRepository.persist(compteWave); + LOG.infof("Compte Wave créé avec succès: ID=%s, Téléphone=%s", compteWave.getId(), compteWave.getNumeroTelephone()); + + return convertToDTO(compteWave); + } + + /** + * Met à jour un compte Wave + * + * @param id ID du compte + * @param compteWaveDTO DTO avec les modifications + * @return DTO du compte mis à jour + */ + @Transactional + public CompteWaveDTO mettreAJourCompteWave(UUID id, CompteWaveDTO compteWaveDTO) { + LOG.infof("Mise à jour du compte Wave ID: %s", id); + + CompteWave compteWave = + compteWaveRepository + .findCompteWaveById(id) + .orElseThrow(() -> new NotFoundException("Compte Wave non trouvé avec l'ID: " + id)); + + updateFromDTO(compteWave, compteWaveDTO); + compteWave.setModifiePar(keycloakService.getCurrentUserEmail()); + + compteWaveRepository.persist(compteWave); + LOG.infof("Compte Wave mis à jour avec succès: ID=%s", id); + + return convertToDTO(compteWave); + } + + /** + * Vérifie un compte Wave + * + * @param id ID du compte + * @return DTO du compte vérifié + */ + @Transactional + public CompteWaveDTO verifierCompteWave(UUID id) { + LOG.infof("Vérification du compte Wave ID: %s", id); + + CompteWave compteWave = + compteWaveRepository + .findCompteWaveById(id) + .orElseThrow(() -> new NotFoundException("Compte Wave non trouvé avec l'ID: " + id)); + + compteWave.setStatutCompte(StatutCompteWave.VERIFIE); + compteWave.setDateDerniereVerification(LocalDateTime.now()); + compteWave.setModifiePar(keycloakService.getCurrentUserEmail()); + + compteWaveRepository.persist(compteWave); + LOG.infof("Compte Wave vérifié avec succès: ID=%s", id); + + return convertToDTO(compteWave); + } + + /** + * Trouve un compte Wave par son ID + * + * @param id ID du compte + * @return DTO du compte + */ + public CompteWaveDTO trouverCompteWaveParId(UUID id) { + return compteWaveRepository + .findCompteWaveById(id) + .map(this::convertToDTO) + .orElseThrow(() -> new NotFoundException("Compte Wave non trouvé avec l'ID: " + id)); + } + + /** + * Trouve un compte Wave par numéro de téléphone + * + * @param numeroTelephone Numéro de téléphone + * @return DTO du compte ou null + */ + public CompteWaveDTO trouverCompteWaveParTelephone(String numeroTelephone) { + return compteWaveRepository + .findByNumeroTelephone(numeroTelephone) + .map(this::convertToDTO) + .orElse(null); + } + + /** + * Liste tous les comptes Wave d'une organisation + * + * @param organisationId ID de l'organisation + * @return Liste des comptes Wave + */ + public List listerComptesWaveParOrganisation(UUID organisationId) { + return compteWaveRepository.findByOrganisationId(organisationId).stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + /** + * Crée une nouvelle transaction Wave + * + * @param transactionWaveDTO DTO de la transaction à créer + * @return DTO de la transaction créée + */ + @Transactional + public TransactionWaveDTO creerTransactionWave(TransactionWaveDTO transactionWaveDTO) { + LOG.infof("Création d'une nouvelle transaction Wave: %s", transactionWaveDTO.getWaveTransactionId()); + + TransactionWave transactionWave = convertToEntity(transactionWaveDTO); + transactionWave.setCreePar(keycloakService.getCurrentUserEmail()); + + transactionWaveRepository.persist(transactionWave); + LOG.infof( + "Transaction Wave créée avec succès: ID=%s, WaveTransactionId=%s", + transactionWave.getId(), transactionWave.getWaveTransactionId()); + + return convertToDTO(transactionWave); + } + + /** + * Met à jour le statut d'une transaction Wave + * + * @param waveTransactionId Identifiant Wave de la transaction + * @param nouveauStatut Nouveau statut + * @return DTO de la transaction mise à jour + */ + @Transactional + public TransactionWaveDTO mettreAJourStatutTransaction( + String waveTransactionId, StatutTransactionWave nouveauStatut) { + LOG.infof("Mise à jour du statut de la transaction Wave: %s -> %s", waveTransactionId, nouveauStatut); + + TransactionWave transactionWave = + transactionWaveRepository + .findByWaveTransactionId(waveTransactionId) + .orElseThrow( + () -> + new NotFoundException( + "Transaction Wave non trouvée avec l'ID: " + waveTransactionId)); + + transactionWave.setStatutTransaction(nouveauStatut); + transactionWave.setDateDerniereTentative(LocalDateTime.now()); + transactionWave.setModifiePar(keycloakService.getCurrentUserEmail()); + + transactionWaveRepository.persist(transactionWave); + LOG.infof("Statut de la transaction Wave mis à jour: %s", waveTransactionId); + + return convertToDTO(transactionWave); + } + + /** + * Trouve une transaction Wave par son identifiant Wave + * + * @param waveTransactionId Identifiant Wave + * @return DTO de la transaction + */ + public TransactionWaveDTO trouverTransactionWaveParId(String waveTransactionId) { + return transactionWaveRepository + .findByWaveTransactionId(waveTransactionId) + .map(this::convertToDTO) + .orElseThrow( + () -> new NotFoundException("Transaction Wave non trouvée avec l'ID: " + waveTransactionId)); + } + + // ======================================== + // MÉTHODES PRIVÉES + // ======================================== + + /** Convertit une entité CompteWave en DTO */ + private CompteWaveDTO convertToDTO(CompteWave compteWave) { + if (compteWave == null) { + return null; + } + + CompteWaveDTO dto = new CompteWaveDTO(); + dto.setId(compteWave.getId()); + dto.setNumeroTelephone(compteWave.getNumeroTelephone()); + dto.setStatutCompte(compteWave.getStatutCompte()); + dto.setWaveAccountId(compteWave.getWaveAccountId()); + dto.setEnvironnement(compteWave.getEnvironnement()); + dto.setDateDerniereVerification(compteWave.getDateDerniereVerification()); + dto.setCommentaire(compteWave.getCommentaire()); + + if (compteWave.getOrganisation() != null) { + dto.setOrganisationId(compteWave.getOrganisation().getId()); + } + if (compteWave.getMembre() != null) { + dto.setMembreId(compteWave.getMembre().getId()); + } + + dto.setDateCreation(compteWave.getDateCreation()); + dto.setDateModification(compteWave.getDateModification()); + dto.setActif(compteWave.getActif()); + + return dto; + } + + /** Convertit un DTO en entité CompteWave */ + private CompteWave convertToEntity(CompteWaveDTO dto) { + if (dto == null) { + return null; + } + + CompteWave compteWave = new CompteWave(); + compteWave.setNumeroTelephone(dto.getNumeroTelephone()); + compteWave.setStatutCompte(dto.getStatutCompte() != null ? dto.getStatutCompte() : StatutCompteWave.NON_VERIFIE); + compteWave.setWaveAccountId(dto.getWaveAccountId()); + compteWave.setEnvironnement(dto.getEnvironnement() != null ? dto.getEnvironnement() : "SANDBOX"); + compteWave.setDateDerniereVerification(dto.getDateDerniereVerification()); + compteWave.setCommentaire(dto.getCommentaire()); + + // Relations + if (dto.getOrganisationId() != null) { + Organisation org = + organisationRepository + .findByIdOptional(dto.getOrganisationId()) + .orElseThrow( + () -> + new NotFoundException( + "Organisation non trouvée avec l'ID: " + dto.getOrganisationId())); + compteWave.setOrganisation(org); + } + + if (dto.getMembreId() != null) { + Membre membre = + membreRepository + .findByIdOptional(dto.getMembreId()) + .orElseThrow( + () -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId())); + compteWave.setMembre(membre); + } + + return compteWave; + } + + /** Met à jour une entité CompteWave à partir d'un DTO */ + private void updateFromDTO(CompteWave compteWave, CompteWaveDTO dto) { + if (dto.getStatutCompte() != null) { + compteWave.setStatutCompte(dto.getStatutCompte()); + } + if (dto.getWaveAccountId() != null) { + compteWave.setWaveAccountId(dto.getWaveAccountId()); + } + if (dto.getEnvironnement() != null) { + compteWave.setEnvironnement(dto.getEnvironnement()); + } + if (dto.getDateDerniereVerification() != null) { + compteWave.setDateDerniereVerification(dto.getDateDerniereVerification()); + } + if (dto.getCommentaire() != null) { + compteWave.setCommentaire(dto.getCommentaire()); + } + } + + /** Convertit une entité TransactionWave en DTO */ + private TransactionWaveDTO convertToDTO(TransactionWave transactionWave) { + if (transactionWave == null) { + return null; + } + + TransactionWaveDTO dto = new TransactionWaveDTO(); + dto.setId(transactionWave.getId()); + dto.setWaveTransactionId(transactionWave.getWaveTransactionId()); + dto.setWaveRequestId(transactionWave.getWaveRequestId()); + dto.setWaveReference(transactionWave.getWaveReference()); + dto.setTypeTransaction(transactionWave.getTypeTransaction()); + dto.setStatutTransaction(transactionWave.getStatutTransaction()); + dto.setMontant(transactionWave.getMontant()); + dto.setFrais(transactionWave.getFrais()); + dto.setMontantNet(transactionWave.getMontantNet()); + dto.setCodeDevise(transactionWave.getCodeDevise()); + dto.setTelephonePayeur(transactionWave.getTelephonePayeur()); + dto.setTelephoneBeneficiaire(transactionWave.getTelephoneBeneficiaire()); + dto.setMetadonnees(transactionWave.getMetadonnees()); + dto.setNombreTentatives(transactionWave.getNombreTentatives()); + dto.setDateDerniereTentative(transactionWave.getDateDerniereTentative()); + dto.setMessageErreur(transactionWave.getMessageErreur()); + + if (transactionWave.getCompteWave() != null) { + dto.setCompteWaveId(transactionWave.getCompteWave().getId()); + } + + dto.setDateCreation(transactionWave.getDateCreation()); + dto.setDateModification(transactionWave.getDateModification()); + dto.setActif(transactionWave.getActif()); + + return dto; + } + + /** Convertit un DTO en entité TransactionWave */ + private TransactionWave convertToEntity(TransactionWaveDTO dto) { + if (dto == null) { + return null; + } + + TransactionWave transactionWave = new TransactionWave(); + transactionWave.setWaveTransactionId(dto.getWaveTransactionId()); + transactionWave.setWaveRequestId(dto.getWaveRequestId()); + transactionWave.setWaveReference(dto.getWaveReference()); + transactionWave.setTypeTransaction(dto.getTypeTransaction()); + transactionWave.setStatutTransaction( + dto.getStatutTransaction() != null + ? dto.getStatutTransaction() + : StatutTransactionWave.INITIALISE); + transactionWave.setMontant(dto.getMontant()); + transactionWave.setFrais(dto.getFrais()); + transactionWave.setMontantNet(dto.getMontantNet()); + transactionWave.setCodeDevise(dto.getCodeDevise() != null ? dto.getCodeDevise() : "XOF"); + transactionWave.setTelephonePayeur(dto.getTelephonePayeur()); + transactionWave.setTelephoneBeneficiaire(dto.getTelephoneBeneficiaire()); + transactionWave.setMetadonnees(dto.getMetadonnees()); + transactionWave.setNombreTentatives(dto.getNombreTentatives() != null ? dto.getNombreTentatives() : 0); + transactionWave.setDateDerniereTentative(dto.getDateDerniereTentative()); + transactionWave.setMessageErreur(dto.getMessageErreur()); + + // Relation CompteWave + if (dto.getCompteWaveId() != null) { + CompteWave compteWave = + compteWaveRepository + .findCompteWaveById(dto.getCompteWaveId()) + .orElseThrow( + () -> + new NotFoundException( + "Compte Wave non trouvé avec l'ID: " + dto.getCompteWaveId())); + transactionWave.setCompteWave(compteWave); + } + + return transactionWave; + } +} diff --git a/src/main/java/dev/lions/unionflow/server/util/IdConverter.java b/src/main/java/dev/lions/unionflow/server/util/IdConverter.java new file mode 100644 index 0000000..bb2b78b --- /dev/null +++ b/src/main/java/dev/lions/unionflow/server/util/IdConverter.java @@ -0,0 +1,150 @@ +package dev.lions.unionflow.server.util; + +import java.util.UUID; + +/** + * Utilitaire pour la conversion entre IDs Long (entités Panache) et UUID (DTOs) + * + *

DÉPRÉCIÉ: Cette classe est maintenant obsolète car toutes les entités + * utilisent désormais UUID directement. Elle est conservée uniquement pour compatibilité + * avec d'éventuels anciens scripts de migration de données. + * + *

Cette classe fournit des méthodes pour convertir de manière cohérente + * entre les identifiants Long utilisés par PanacheEntity et les UUID utilisés + * par les DTOs de l'API. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-16 + * @deprecated Depuis la migration UUID complète (2025-01-16). Utilisez directement UUID dans toutes les entités. + */ +@Deprecated(since = "2025-01-16", forRemoval = true) +public final class IdConverter { + + private IdConverter() { + // Classe utilitaire - constructeur privé + } + + /** + * Convertit un ID Long en UUID de manière déterministe + * + *

DÉPRÉCIÉ: Utilisez directement UUID dans vos entités. + * + *

Utilise un namespace UUID fixe pour garantir la cohérence et éviter les collisions. + * Le même Long produira toujours le même UUID. + * + * @param entityType Le type d'entité (ex: "membre", "organisation", "cotisation") + * @param id L'ID Long de l'entité + * @return L'UUID correspondant, ou null si id est null + * @deprecated Utilisez directement UUID dans vos entités + */ + @Deprecated + public static UUID longToUUID(String entityType, Long id) { + if (id == null) { + return null; + } + + // Utilisation d'un namespace UUID fixe par type d'entité pour garantir la cohérence + UUID namespace = getNamespaceForEntityType(entityType); + String name = entityType + "-" + id; + return UUID.nameUUIDFromBytes((namespace.toString() + name).getBytes()); + } + + /** + * Convertit un UUID en ID Long approximatif + * + *

DÉPRÉCIÉ: Utilisez directement UUID dans vos entités. + * + *

ATTENTION: Cette conversion n'est pas parfaitement réversible car UUID → Long + * perd de l'information. Cette méthode est principalement utilisée pour la recherche + * approximative. Pour une conversion réversible, il faudrait stocker le mapping dans la DB. + * + * @param uuid L'UUID à convertir + * @return Une approximation de l'ID Long, ou null si uuid est null + * @deprecated Utilisez directement UUID dans vos entités + */ + @Deprecated + public static Long uuidToLong(UUID uuid) { + if (uuid == null) { + return null; + } + + // Extraction d'une approximation de Long depuis les bits de l'UUID + // Cette méthode n'est pas parfaitement réversible + long mostSignificantBits = uuid.getMostSignificantBits(); + long leastSignificantBits = uuid.getLeastSignificantBits(); + + // Combinaison des bits pour obtenir un Long + // Utilisation de XOR pour mélanger les bits + long combined = mostSignificantBits ^ leastSignificantBits; + + // Conversion en valeur positive + return Math.abs(combined); + } + + /** + * Obtient le namespace UUID pour un type d'entité donné + * + * @param entityType Le type d'entité + * @return Le namespace UUID correspondant + */ + private static UUID getNamespaceForEntityType(String entityType) { + return switch (entityType.toLowerCase()) { + case "membre" -> UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8"); + case "organisation" -> UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); + case "cotisation" -> UUID.fromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8"); + case "evenement" -> UUID.fromString("6ba7b813-9dad-11d1-80b4-00c04fd430c8"); + case "demandeaide" -> UUID.fromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8"); + case "inscriptionevenement" -> UUID.fromString("6ba7b815-9dad-11d1-80b4-00c04fd430c8"); + default -> UUID.fromString("6ba7b816-9dad-11d1-80b4-00c04fd430c8"); // Namespace par défaut + }; + } + + /** + * Convertit un ID Long d'organisation en UUID pour le DTO + * + * @param organisationId L'ID Long de l'organisation + * @return L'UUID correspondant, ou null si organisationId est null + * @deprecated Utilisez directement UUID dans vos entités + */ + @Deprecated + public static UUID organisationIdToUUID(Long organisationId) { + return longToUUID("organisation", organisationId); + } + + /** + * Convertit un ID Long de membre en UUID pour le DTO + * + * @param membreId L'ID Long du membre + * @return L'UUID correspondant, ou null si membreId est null + * @deprecated Utilisez directement UUID dans vos entités + */ + @Deprecated + public static UUID membreIdToUUID(Long membreId) { + return longToUUID("membre", membreId); + } + + /** + * Convertit un ID Long de cotisation en UUID pour le DTO + * + * @param cotisationId L'ID Long de la cotisation + * @return L'UUID correspondant, ou null si cotisationId est null + * @deprecated Utilisez directement UUID dans vos entités + */ + @Deprecated + public static UUID cotisationIdToUUID(Long cotisationId) { + return longToUUID("cotisation", cotisationId); + } + + /** + * Convertit un ID Long d'événement en UUID pour le DTO + * + * @param evenementId L'ID Long de l'événement + * @return L'UUID correspondant, ou null si evenementId est null + * @deprecated Utilisez directement UUID dans vos entités + */ + @Deprecated + public static UUID evenementIdToUUID(Long evenementId) { + return longToUUID("evenement", evenementId); + } +} diff --git a/src/main/resources/META-INF/beans.xml b/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000..1ba4e60 --- /dev/null +++ b/src/main/resources/META-INF/beans.xml @@ -0,0 +1,8 @@ + + + diff --git a/src/main/resources/application-minimal.properties b/src/main/resources/application-minimal.properties new file mode 100644 index 0000000..309e021 --- /dev/null +++ b/src/main/resources/application-minimal.properties @@ -0,0 +1,56 @@ +# Configuration UnionFlow Server - Mode Minimal +quarkus.application.name=unionflow-server-minimal +quarkus.application.version=1.0.0 + +# Configuration HTTP +quarkus.http.port=8080 +quarkus.http.host=0.0.0.0 + +# Configuration CORS +quarkus.http.cors=true +quarkus.http.cors.origins=* +quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS +quarkus.http.cors.headers=Content-Type,Authorization + +# Configuration Base de données H2 (en mémoire) +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password= +quarkus.datasource.jdbc.url=jdbc:h2:mem:unionflow_minimal;DB_CLOSE_DELAY=-1;MODE=PostgreSQL + +# Configuration Hibernate +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.jdbc.timezone=UTC +quarkus.hibernate-orm.packages=dev.lions.unionflow.server.entity + +# Désactiver Flyway +quarkus.flyway.migrate-at-start=false + +# Désactiver Keycloak temporairement +quarkus.oidc.tenant-enabled=false + +# Chemins publics (tous publics en mode minimal) +quarkus.http.auth.permission.public.paths=/* +quarkus.http.auth.permission.public.policy=permit + +# Configuration OpenAPI +quarkus.smallrye-openapi.info-title=UnionFlow Server API - Minimal +quarkus.smallrye-openapi.info-version=1.0.0 +quarkus.smallrye-openapi.info-description=API REST pour la gestion d'union (mode minimal) +quarkus.smallrye-openapi.servers=http://localhost:8080 + +# Configuration Swagger UI +quarkus.swagger-ui.always-include=true +quarkus.swagger-ui.path=/swagger-ui + +# Configuration santé +quarkus.smallrye-health.root-path=/health + +# Configuration logging +quarkus.log.console.enable=true +quarkus.log.console.level=INFO +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.category."dev.lions.unionflow".level=DEBUG +quarkus.log.category."org.hibernate".level=WARN +quarkus.log.category."io.quarkus".level=INFO diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..d1dc9c8 --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,77 @@ +# Configuration UnionFlow Server - PRODUCTION +# Ce fichier est utilisé avec le profil Quarkus "prod" + +# Configuration HTTP +quarkus.http.port=8085 +quarkus.http.host=0.0.0.0 + +# Configuration CORS - Production (strict) +quarkus.http.cors=true +quarkus.http.cors.origins=${CORS_ORIGINS:https://unionflow.lions.dev,https://security.lions.dev} +quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS +quarkus.http.cors.headers=Content-Type,Authorization +quarkus.http.cors.allow-credentials=true + +# Configuration Base de données PostgreSQL - Production +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=${DB_USERNAME:unionflow} +quarkus.datasource.password=${DB_PASSWORD} +quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://localhost:5432/unionflow} +quarkus.datasource.jdbc.min-size=5 +quarkus.datasource.jdbc.max-size=20 + +# Configuration Hibernate - Production (IMPORTANT: update, pas drop-and-create) +quarkus.hibernate-orm.database.generation=update +quarkus.hibernate-orm.log.sql=false +quarkus.hibernate-orm.jdbc.timezone=UTC +quarkus.hibernate-orm.packages=dev.lions.unionflow.server.entity +quarkus.hibernate-orm.metrics.enabled=false + +# Configuration Flyway - Production (ACTIVÉ) +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=1.0.0 + +# Configuration Keycloak OIDC - Production +quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/unionflow} +quarkus.oidc.client-id=unionflow-server +quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} +quarkus.oidc.tls.verification=required +quarkus.oidc.application-type=service + +# Configuration Keycloak Policy Enforcer +quarkus.keycloak.policy-enforcer.enable=false +quarkus.keycloak.policy-enforcer.lazy-load-paths=true +quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE + +# Chemins publics (non protégés) +quarkus.http.auth.permission.public.paths=/health,/q/*,/favicon.ico +quarkus.http.auth.permission.public.policy=permit + +# Configuration OpenAPI - Production (Swagger désactivé ou protégé) +quarkus.smallrye-openapi.info-title=UnionFlow Server API +quarkus.smallrye-openapi.info-version=1.0.0 +quarkus.smallrye-openapi.info-description=API REST pour la gestion d'union avec authentification Keycloak +quarkus.smallrye-openapi.servers=https://api.lions.dev/unionflow + +# Configuration Swagger UI - Production (DÉSACTIVÉ pour sécurité) +quarkus.swagger-ui.always-include=false + +# Configuration santé +quarkus.smallrye-health.root-path=/health + +# Configuration logging - Production +quarkus.log.console.enable=true +quarkus.log.console.level=INFO +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.category."dev.lions.unionflow".level=INFO +quarkus.log.category."org.hibernate".level=WARN +quarkus.log.category."io.quarkus".level=INFO +quarkus.log.category."org.jboss.resteasy".level=WARN + +# Configuration Wave Money - Production +wave.api.key=${WAVE_API_KEY:} +wave.api.secret=${WAVE_API_SECRET:} +wave.api.base.url=${WAVE_API_BASE_URL:https://api.wave.com/v1} +wave.environment=${WAVE_ENVIRONMENT:production} +wave.webhook.secret=${WAVE_WEBHOOK_SECRET:} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties new file mode 100644 index 0000000..173d6db --- /dev/null +++ b/src/main/resources/application-test.properties @@ -0,0 +1,31 @@ +# Configuration UnionFlow Server - Profil Test +# Ce fichier est chargé automatiquement quand le profil 'test' est actif + +# Configuration Base de données H2 pour tests +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password= +quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL + +# Configuration Hibernate pour tests +quarkus.hibernate-orm.database.generation=drop-and-create +# Désactiver complètement l'exécution des scripts SQL au démarrage +quarkus.hibernate-orm.sql-load-script-source=none +# Empêcher Hibernate d'exécuter les scripts SQL automatiquement +# Note: Ne pas définir quarkus.hibernate-orm.sql-load-script car une chaîne vide peut causer des problèmes + +# Configuration Flyway pour tests (désactivé complètement) +quarkus.flyway.migrate-at-start=false +quarkus.flyway.enabled=false +quarkus.flyway.baseline-on-migrate=false +# Note: Ne pas définir quarkus.flyway.locations car une chaîne vide cause une erreur de configuration + +# Configuration Keycloak pour tests (désactivé) +quarkus.oidc.tenant-enabled=false +quarkus.keycloak.policy-enforcer.enable=false + +# Configuration HTTP pour tests +quarkus.http.port=0 +quarkus.http.test-port=0 + + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..c81a866 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,103 @@ +# Configuration UnionFlow Server +quarkus.application.name=unionflow-server +quarkus.application.version=1.0.0 + +# Configuration HTTP +quarkus.http.port=8085 +quarkus.http.host=0.0.0.0 + +# Configuration CORS +quarkus.http.cors=true +quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:8086,https://unionflow.lions.dev,https://security.lions.dev} +quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS +quarkus.http.cors.headers=Content-Type,Authorization + +# Configuration Base de données PostgreSQL (par défaut) +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=${DB_USERNAME:unionflow} +quarkus.datasource.password=${DB_PASSWORD} +quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://localhost:5432/unionflow} +quarkus.datasource.jdbc.min-size=2 +quarkus.datasource.jdbc.max-size=10 + +# Configuration Base de données PostgreSQL pour développement +%dev.quarkus.datasource.username=skyfile +%dev.quarkus.datasource.password=${DB_PASSWORD_DEV:skyfile} +%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/unionflow + +# Configuration Hibernate +quarkus.hibernate-orm.database.generation=update +quarkus.hibernate-orm.log.sql=false +quarkus.hibernate-orm.jdbc.timezone=UTC +quarkus.hibernate-orm.packages=dev.lions.unionflow.server.entity +# Désactiver l'avertissement PanacheEntity (nous utilisons BaseEntity personnalisé) +quarkus.hibernate-orm.metrics.enabled=false + +# Configuration Hibernate pour développement +%dev.quarkus.hibernate-orm.database.generation=drop-and-create +%dev.quarkus.hibernate-orm.sql-load-script=import.sql +%dev.quarkus.hibernate-orm.log.sql=true + +# Configuration Flyway pour migrations +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=1.0.0 + +# Configuration Flyway pour développement (désactivé) +%dev.quarkus.flyway.migrate-at-start=false + +# Configuration Keycloak OIDC (par défaut) +quarkus.oidc.auth-server-url=http://localhost:8180/realms/unionflow +quarkus.oidc.client-id=unionflow-server +quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} +quarkus.oidc.tls.verification=none +quarkus.oidc.application-type=service + +# Configuration Keycloak pour développement +%dev.quarkus.oidc.tenant-enabled=false +%dev.quarkus.oidc.auth-server-url=http://localhost:8180/realms/unionflow + +# Configuration Keycloak Policy Enforcer (temporairement désactivé) +quarkus.keycloak.policy-enforcer.enable=false +quarkus.keycloak.policy-enforcer.lazy-load-paths=true +quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE + +# Chemins publics (non protégés) +quarkus.http.auth.permission.public.paths=/health,/q/*,/favicon.ico,/auth/callback,/auth/* +quarkus.http.auth.permission.public.policy=permit + +# Configuration OpenAPI +quarkus.smallrye-openapi.info-title=UnionFlow Server API +quarkus.smallrye-openapi.info-version=1.0.0 +quarkus.smallrye-openapi.info-description=API REST pour la gestion d'union avec authentification Keycloak +quarkus.smallrye-openapi.servers=http://localhost:8085 + +# Configuration Swagger UI +quarkus.swagger-ui.always-include=true +quarkus.swagger-ui.path=/swagger-ui + +# Configuration santé +quarkus.smallrye-health.root-path=/health + +# Configuration logging +quarkus.log.console.enable=true +quarkus.log.console.level=INFO +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.category."dev.lions.unionflow".level=INFO +quarkus.log.category."org.hibernate".level=WARN +quarkus.log.category."io.quarkus".level=INFO + +# Configuration logging pour développement +%dev.quarkus.log.category."dev.lions.unionflow".level=DEBUG +%dev.quarkus.log.category."org.hibernate.SQL".level=DEBUG + +# Configuration Jandex pour résoudre les warnings de réflexion +quarkus.index-dependency.unionflow-server-api.group-id=dev.lions.unionflow +quarkus.index-dependency.unionflow-server-api.artifact-id=unionflow-server-api + +# Configuration Wave Money +wave.api.key=${WAVE_API_KEY:} +wave.api.secret=${WAVE_API_SECRET:} +wave.api.base.url=${WAVE_API_BASE_URL:https://api.wave.com/v1} +wave.environment=${WAVE_ENVIRONMENT:sandbox} +wave.webhook.secret=${WAVE_WEBHOOK_SECRET:} diff --git a/src/main/resources/db/migration/V1.2__Create_Organisation_Table.sql b/src/main/resources/db/migration/V1.2__Create_Organisation_Table.sql new file mode 100644 index 0000000..7329794 --- /dev/null +++ b/src/main/resources/db/migration/V1.2__Create_Organisation_Table.sql @@ -0,0 +1,143 @@ +-- Migration V1.2: Création de la table organisations +-- Auteur: UnionFlow Team +-- Date: 2025-01-15 +-- Description: Création de la table organisations avec toutes les colonnes nécessaires + +-- Création de la table organisations +CREATE TABLE organisations ( + id BIGSERIAL PRIMARY KEY, + + -- Informations de base + nom VARCHAR(200) NOT NULL, + nom_court VARCHAR(50), + type_organisation VARCHAR(50) NOT NULL DEFAULT 'ASSOCIATION', + statut VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', + description TEXT, + date_fondation DATE, + numero_enregistrement VARCHAR(100) UNIQUE, + + -- Informations de contact + email VARCHAR(255) NOT NULL UNIQUE, + telephone VARCHAR(20), + telephone_secondaire VARCHAR(20), + email_secondaire VARCHAR(255), + + -- Adresse + adresse VARCHAR(500), + ville VARCHAR(100), + code_postal VARCHAR(20), + region VARCHAR(100), + pays VARCHAR(100), + + -- Coordonnées géographiques + latitude DECIMAL(9,6) CHECK (latitude >= -90 AND latitude <= 90), + longitude DECIMAL(9,6) CHECK (longitude >= -180 AND longitude <= 180), + + -- Web et réseaux sociaux + site_web VARCHAR(500), + logo VARCHAR(500), + reseaux_sociaux VARCHAR(1000), + + -- Hiérarchie + organisation_parente_id UUID, + niveau_hierarchique INTEGER NOT NULL DEFAULT 0, + + -- Statistiques + nombre_membres INTEGER NOT NULL DEFAULT 0, + nombre_administrateurs INTEGER NOT NULL DEFAULT 0, + + -- Finances + budget_annuel DECIMAL(14,2) CHECK (budget_annuel >= 0), + devise VARCHAR(3) DEFAULT 'XOF', + cotisation_obligatoire BOOLEAN NOT NULL DEFAULT FALSE, + montant_cotisation_annuelle DECIMAL(12,2) CHECK (montant_cotisation_annuelle >= 0), + + -- Informations complémentaires + objectifs TEXT, + activites_principales TEXT, + certifications VARCHAR(500), + partenaires VARCHAR(1000), + notes VARCHAR(1000), + + -- Paramètres + organisation_publique BOOLEAN NOT NULL DEFAULT TRUE, + accepte_nouveaux_membres BOOLEAN NOT NULL DEFAULT TRUE, + + -- Métadonnées + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(100), + modifie_par VARCHAR(100), + version BIGINT NOT NULL DEFAULT 0, + + -- Contraintes + CONSTRAINT chk_organisation_statut CHECK (statut IN ('ACTIVE', 'SUSPENDUE', 'DISSOUTE', 'EN_ATTENTE')), + CONSTRAINT chk_organisation_type CHECK (type_organisation IN ( + 'ASSOCIATION', 'LIONS_CLUB', 'ROTARY_CLUB', 'COOPERATIVE', + 'FONDATION', 'ONG', 'SYNDICAT', 'AUTRE' + )), + CONSTRAINT chk_organisation_devise CHECK (devise IN ('XOF', 'EUR', 'USD', 'GBP', 'CHF')), + CONSTRAINT chk_organisation_niveau CHECK (niveau_hierarchique >= 0 AND niveau_hierarchique <= 10), + CONSTRAINT chk_organisation_membres CHECK (nombre_membres >= 0), + CONSTRAINT chk_organisation_admins CHECK (nombre_administrateurs >= 0) +); + +-- Création des index pour optimiser les performances +CREATE INDEX idx_organisation_nom ON organisations(nom); +CREATE INDEX idx_organisation_email ON organisations(email); +CREATE INDEX idx_organisation_statut ON organisations(statut); +CREATE INDEX idx_organisation_type ON organisations(type_organisation); +CREATE INDEX idx_organisation_ville ON organisations(ville); +CREATE INDEX idx_organisation_pays ON organisations(pays); +CREATE INDEX idx_organisation_parente ON organisations(organisation_parente_id); +CREATE INDEX idx_organisation_numero_enregistrement ON organisations(numero_enregistrement); +CREATE INDEX idx_organisation_actif ON organisations(actif); +CREATE INDEX idx_organisation_date_creation ON organisations(date_creation); +CREATE INDEX idx_organisation_publique ON organisations(organisation_publique); +CREATE INDEX idx_organisation_accepte_membres ON organisations(accepte_nouveaux_membres); + +-- Index composites pour les recherches fréquentes +CREATE INDEX idx_organisation_statut_actif ON organisations(statut, actif); +CREATE INDEX idx_organisation_type_ville ON organisations(type_organisation, ville); +CREATE INDEX idx_organisation_pays_region ON organisations(pays, region); +CREATE INDEX idx_organisation_publique_actif ON organisations(organisation_publique, actif); + +-- Index pour les recherches textuelles +CREATE INDEX idx_organisation_nom_lower ON organisations(LOWER(nom)); +CREATE INDEX idx_organisation_nom_court_lower ON organisations(LOWER(nom_court)); +CREATE INDEX idx_organisation_ville_lower ON organisations(LOWER(ville)); + +-- Ajout de la colonne organisation_id à la table membres (si elle n'existe pas déjà) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'membres' AND column_name = 'organisation_id' + ) THEN + ALTER TABLE membres ADD COLUMN organisation_id BIGINT; + ALTER TABLE membres ADD CONSTRAINT fk_membre_organisation + FOREIGN KEY (organisation_id) REFERENCES organisations(id); + CREATE INDEX idx_membre_organisation ON membres(organisation_id); + END IF; +END $$; + +-- IMPORTANT: Aucune donnée fictive n'est insérée dans ce script de migration. +-- Les données doivent être insérées manuellement via l'interface d'administration +-- ou via des scripts de migration séparés si nécessaire pour la production. + +-- Mise à jour des statistiques de la base de données +ANALYZE organisations; + +-- Commentaires sur la table et les colonnes principales +COMMENT ON TABLE organisations IS 'Table des organisations (Lions Clubs, Associations, Coopératives, etc.)'; +COMMENT ON COLUMN organisations.nom IS 'Nom officiel de l''organisation'; +COMMENT ON COLUMN organisations.nom_court IS 'Nom court ou sigle de l''organisation'; +COMMENT ON COLUMN organisations.type_organisation IS 'Type d''organisation (LIONS_CLUB, ASSOCIATION, etc.)'; +COMMENT ON COLUMN organisations.statut IS 'Statut actuel de l''organisation (ACTIVE, SUSPENDUE, etc.)'; +COMMENT ON COLUMN organisations.organisation_parente_id IS 'ID de l''organisation parente pour la hiérarchie'; +COMMENT ON COLUMN organisations.niveau_hierarchique IS 'Niveau dans la hiérarchie (0 = racine)'; +COMMENT ON COLUMN organisations.nombre_membres IS 'Nombre total de membres actifs'; +COMMENT ON COLUMN organisations.organisation_publique IS 'Si l''organisation est visible publiquement'; +COMMENT ON COLUMN organisations.accepte_nouveaux_membres IS 'Si l''organisation accepte de nouveaux membres'; +COMMENT ON COLUMN organisations.version IS 'Version pour le contrôle de concurrence optimiste'; diff --git a/src/main/resources/db/migration/V1.3__Convert_Ids_To_UUID.sql b/src/main/resources/db/migration/V1.3__Convert_Ids_To_UUID.sql new file mode 100644 index 0000000..c921d22 --- /dev/null +++ b/src/main/resources/db/migration/V1.3__Convert_Ids_To_UUID.sql @@ -0,0 +1,419 @@ +-- Migration V1.3: Conversion des colonnes ID de BIGINT vers UUID +-- Auteur: UnionFlow Team +-- Date: 2025-01-16 +-- Description: Convertit toutes les colonnes ID et clés étrangères de BIGINT vers UUID +-- ATTENTION: Cette migration supprime toutes les données existantes pour simplifier la conversion +-- Pour une migration avec préservation des données, voir V1.3.1__Convert_Ids_To_UUID_With_Data.sql + +-- ============================================ +-- ÉTAPE 1: Suppression des contraintes de clés étrangères +-- ============================================ + +-- Supprimer les contraintes de clés étrangères existantes +DO $$ +BEGIN + -- Supprimer FK membres -> organisations + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'fk_membre_organisation' + AND table_name = 'membres' + ) THEN + ALTER TABLE membres DROP CONSTRAINT fk_membre_organisation; + END IF; + + -- Supprimer FK cotisations -> membres + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name LIKE 'fk_cotisation%' + AND table_name = 'cotisations' + ) THEN + ALTER TABLE cotisations DROP CONSTRAINT IF EXISTS fk_cotisation_membre CASCADE; + END IF; + + -- Supprimer FK evenements -> organisations + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name LIKE 'fk_evenement%' + AND table_name = 'evenements' + ) THEN + ALTER TABLE evenements DROP CONSTRAINT IF EXISTS fk_evenement_organisation CASCADE; + END IF; + + -- Supprimer FK inscriptions_evenement -> membres et evenements + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name LIKE 'fk_inscription%' + AND table_name = 'inscriptions_evenement' + ) THEN + ALTER TABLE inscriptions_evenement DROP CONSTRAINT IF EXISTS fk_inscription_membre CASCADE; + ALTER TABLE inscriptions_evenement DROP CONSTRAINT IF EXISTS fk_inscription_evenement CASCADE; + END IF; + + -- Supprimer FK demandes_aide -> membres et organisations + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name LIKE 'fk_demande%' + AND table_name = 'demandes_aide' + ) THEN + ALTER TABLE demandes_aide DROP CONSTRAINT IF EXISTS fk_demande_demandeur CASCADE; + ALTER TABLE demandes_aide DROP CONSTRAINT IF EXISTS fk_demande_evaluateur CASCADE; + ALTER TABLE demandes_aide DROP CONSTRAINT IF EXISTS fk_demande_organisation CASCADE; + END IF; +END $$; + +-- ============================================ +-- ÉTAPE 2: Supprimer les séquences (BIGSERIAL) +-- ============================================ + +DROP SEQUENCE IF EXISTS membres_SEQ CASCADE; +DROP SEQUENCE IF EXISTS cotisations_SEQ CASCADE; +DROP SEQUENCE IF EXISTS evenements_SEQ CASCADE; +DROP SEQUENCE IF EXISTS organisations_id_seq CASCADE; + +-- ============================================ +-- ÉTAPE 3: Supprimer les tables existantes (pour recréation avec UUID) +-- ============================================ + +-- Supprimer les tables dans l'ordre inverse des dépendances +DROP TABLE IF EXISTS inscriptions_evenement CASCADE; +DROP TABLE IF EXISTS demandes_aide CASCADE; +DROP TABLE IF EXISTS cotisations CASCADE; +DROP TABLE IF EXISTS evenements CASCADE; +DROP TABLE IF EXISTS membres CASCADE; +DROP TABLE IF EXISTS organisations CASCADE; + +-- ============================================ +-- ÉTAPE 4: Recréer les tables avec UUID +-- ============================================ + +-- Table organisations avec UUID +CREATE TABLE organisations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Informations de base + nom VARCHAR(200) NOT NULL, + nom_court VARCHAR(50), + type_organisation VARCHAR(50) NOT NULL DEFAULT 'ASSOCIATION', + statut VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', + description TEXT, + date_fondation DATE, + numero_enregistrement VARCHAR(100) UNIQUE, + + -- Informations de contact + email VARCHAR(255) NOT NULL UNIQUE, + telephone VARCHAR(20), + telephone_secondaire VARCHAR(20), + email_secondaire VARCHAR(255), + + -- Adresse + adresse VARCHAR(500), + ville VARCHAR(100), + code_postal VARCHAR(20), + region VARCHAR(100), + pays VARCHAR(100), + + -- Coordonnées géographiques + latitude DECIMAL(9,6) CHECK (latitude >= -90 AND latitude <= 90), + longitude DECIMAL(9,6) CHECK (longitude >= -180 AND longitude <= 180), + + -- Web et réseaux sociaux + site_web VARCHAR(500), + logo VARCHAR(500), + reseaux_sociaux VARCHAR(1000), + + -- Hiérarchie + organisation_parente_id UUID, + niveau_hierarchique INTEGER NOT NULL DEFAULT 0, + + -- Statistiques + nombre_membres INTEGER NOT NULL DEFAULT 0, + nombre_administrateurs INTEGER NOT NULL DEFAULT 0, + + -- Finances + budget_annuel DECIMAL(14,2) CHECK (budget_annuel >= 0), + devise VARCHAR(3) DEFAULT 'XOF', + cotisation_obligatoire BOOLEAN NOT NULL DEFAULT FALSE, + montant_cotisation_annuelle DECIMAL(12,2) CHECK (montant_cotisation_annuelle >= 0), + + -- Informations complémentaires + objectifs TEXT, + activites_principales TEXT, + certifications VARCHAR(500), + partenaires VARCHAR(1000), + notes VARCHAR(1000), + + -- Paramètres + organisation_publique BOOLEAN NOT NULL DEFAULT TRUE, + accepte_nouveaux_membres BOOLEAN NOT NULL DEFAULT TRUE, + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + -- Contraintes + CONSTRAINT chk_organisation_statut CHECK (statut IN ('ACTIVE', 'SUSPENDUE', 'DISSOUTE', 'EN_ATTENTE')), + CONSTRAINT chk_organisation_type CHECK (type_organisation IN ( + 'ASSOCIATION', 'LIONS_CLUB', 'ROTARY_CLUB', 'COOPERATIVE', + 'FONDATION', 'ONG', 'SYNDICAT', 'AUTRE' + )), + CONSTRAINT chk_organisation_devise CHECK (devise IN ('XOF', 'EUR', 'USD', 'GBP', 'CHF')), + CONSTRAINT chk_organisation_niveau CHECK (niveau_hierarchique >= 0 AND niveau_hierarchique <= 10), + CONSTRAINT chk_organisation_membres CHECK (nombre_membres >= 0), + CONSTRAINT chk_organisation_admins CHECK (nombre_administrateurs >= 0), + + -- Clé étrangère pour hiérarchie + CONSTRAINT fk_organisation_parente FOREIGN KEY (organisation_parente_id) + REFERENCES organisations(id) ON DELETE SET NULL +); + +-- Table membres avec UUID +CREATE TABLE membres ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + numero_membre VARCHAR(20) UNIQUE NOT NULL, + prenom VARCHAR(100) NOT NULL, + nom VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + mot_de_passe VARCHAR(255), + telephone VARCHAR(20), + date_naissance DATE NOT NULL, + date_adhesion DATE NOT NULL, + roles VARCHAR(500), + + -- Clé étrangère vers organisations + organisation_id UUID, + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_membre_organisation FOREIGN KEY (organisation_id) + REFERENCES organisations(id) ON DELETE SET NULL +); + +-- Table cotisations avec UUID +CREATE TABLE cotisations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + numero_reference VARCHAR(50) UNIQUE NOT NULL, + membre_id UUID NOT NULL, + type_cotisation VARCHAR(50) NOT NULL, + montant_du DECIMAL(12,2) NOT NULL CHECK (montant_du >= 0), + montant_paye DECIMAL(12,2) NOT NULL DEFAULT 0 CHECK (montant_paye >= 0), + code_devise VARCHAR(3) NOT NULL DEFAULT 'XOF', + statut VARCHAR(30) NOT NULL, + date_echeance DATE NOT NULL, + date_paiement TIMESTAMP, + description VARCHAR(500), + periode VARCHAR(20), + annee INTEGER NOT NULL CHECK (annee >= 2020 AND annee <= 2100), + mois INTEGER CHECK (mois >= 1 AND mois <= 12), + observations VARCHAR(1000), + recurrente BOOLEAN NOT NULL DEFAULT FALSE, + nombre_rappels INTEGER NOT NULL DEFAULT 0 CHECK (nombre_rappels >= 0), + date_dernier_rappel TIMESTAMP, + valide_par_id UUID, + nom_validateur VARCHAR(100), + date_validation TIMESTAMP, + methode_paiement VARCHAR(50), + reference_paiement VARCHAR(100), + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_cotisation_membre FOREIGN KEY (membre_id) + REFERENCES membres(id) ON DELETE CASCADE, + CONSTRAINT chk_cotisation_statut CHECK (statut IN ('EN_ATTENTE', 'PAYEE', 'EN_RETARD', 'PARTIELLEMENT_PAYEE', 'ANNULEE')), + CONSTRAINT chk_cotisation_devise CHECK (code_devise ~ '^[A-Z]{3}$') +); + +-- Table evenements avec UUID +CREATE TABLE evenements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + titre VARCHAR(200) NOT NULL, + description VARCHAR(2000), + date_debut TIMESTAMP NOT NULL, + date_fin TIMESTAMP, + lieu VARCHAR(255) NOT NULL, + adresse VARCHAR(500), + ville VARCHAR(100), + pays VARCHAR(100), + code_postal VARCHAR(20), + latitude DECIMAL(9,6), + longitude DECIMAL(9,6), + type_evenement VARCHAR(50) NOT NULL, + statut VARCHAR(50) NOT NULL, + url_inscription VARCHAR(500), + url_informations VARCHAR(500), + image_url VARCHAR(500), + capacite_max INTEGER, + cout_participation DECIMAL(12,2), + devise VARCHAR(3), + est_public BOOLEAN NOT NULL DEFAULT TRUE, + tags VARCHAR(500), + notes VARCHAR(1000), + + -- Clé étrangère vers organisations + organisation_id UUID, + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_evenement_organisation FOREIGN KEY (organisation_id) + REFERENCES organisations(id) ON DELETE SET NULL +); + +-- Table inscriptions_evenement avec UUID +CREATE TABLE inscriptions_evenement ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + membre_id UUID NOT NULL, + evenement_id UUID NOT NULL, + date_inscription TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + statut VARCHAR(20) DEFAULT 'CONFIRMEE', + commentaire VARCHAR(500), + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_inscription_membre FOREIGN KEY (membre_id) + REFERENCES membres(id) ON DELETE CASCADE, + CONSTRAINT fk_inscription_evenement FOREIGN KEY (evenement_id) + REFERENCES evenements(id) ON DELETE CASCADE, + CONSTRAINT chk_inscription_statut CHECK (statut IN ('CONFIRMEE', 'EN_ATTENTE', 'ANNULEE', 'REFUSEE')), + CONSTRAINT uk_inscription_membre_evenement UNIQUE (membre_id, evenement_id) +); + +-- Table demandes_aide avec UUID +CREATE TABLE demandes_aide ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + titre VARCHAR(200) NOT NULL, + description TEXT NOT NULL, + type_aide VARCHAR(50) NOT NULL, + statut VARCHAR(50) NOT NULL, + montant_demande DECIMAL(10,2), + montant_approuve DECIMAL(10,2), + date_demande TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_evaluation TIMESTAMP, + date_versement TIMESTAMP, + justification TEXT, + commentaire_evaluation TEXT, + urgence BOOLEAN NOT NULL DEFAULT FALSE, + documents_fournis VARCHAR(500), + + -- Clés étrangères + demandeur_id UUID NOT NULL, + evaluateur_id UUID, + organisation_id UUID NOT NULL, + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_demande_demandeur FOREIGN KEY (demandeur_id) + REFERENCES membres(id) ON DELETE CASCADE, + CONSTRAINT fk_demande_evaluateur FOREIGN KEY (evaluateur_id) + REFERENCES membres(id) ON DELETE SET NULL, + CONSTRAINT fk_demande_organisation FOREIGN KEY (organisation_id) + REFERENCES organisations(id) ON DELETE CASCADE +); + +-- ============================================ +-- ÉTAPE 5: Recréer les index +-- ============================================ + +-- Index pour organisations +CREATE INDEX idx_organisation_nom ON organisations(nom); +CREATE INDEX idx_organisation_email ON organisations(email); +CREATE INDEX idx_organisation_statut ON organisations(statut); +CREATE INDEX idx_organisation_type ON organisations(type_organisation); +CREATE INDEX idx_organisation_ville ON organisations(ville); +CREATE INDEX idx_organisation_pays ON organisations(pays); +CREATE INDEX idx_organisation_parente ON organisations(organisation_parente_id); +CREATE INDEX idx_organisation_numero_enregistrement ON organisations(numero_enregistrement); +CREATE INDEX idx_organisation_actif ON organisations(actif); +CREATE INDEX idx_organisation_date_creation ON organisations(date_creation); +CREATE INDEX idx_organisation_publique ON organisations(organisation_publique); +CREATE INDEX idx_organisation_accepte_membres ON organisations(accepte_nouveaux_membres); +CREATE INDEX idx_organisation_statut_actif ON organisations(statut, actif); + +-- Index pour membres +CREATE INDEX idx_membre_email ON membres(email); +CREATE INDEX idx_membre_numero ON membres(numero_membre); +CREATE INDEX idx_membre_actif ON membres(actif); +CREATE INDEX idx_membre_organisation ON membres(organisation_id); + +-- Index pour cotisations +CREATE INDEX idx_cotisation_membre ON cotisations(membre_id); +CREATE INDEX idx_cotisation_reference ON cotisations(numero_reference); +CREATE INDEX idx_cotisation_statut ON cotisations(statut); +CREATE INDEX idx_cotisation_echeance ON cotisations(date_echeance); +CREATE INDEX idx_cotisation_type ON cotisations(type_cotisation); +CREATE INDEX idx_cotisation_annee_mois ON cotisations(annee, mois); + +-- Index pour evenements +CREATE INDEX idx_evenement_date_debut ON evenements(date_debut); +CREATE INDEX idx_evenement_statut ON evenements(statut); +CREATE INDEX idx_evenement_type ON evenements(type_evenement); +CREATE INDEX idx_evenement_organisation ON evenements(organisation_id); + +-- Index pour inscriptions_evenement +CREATE INDEX idx_inscription_membre ON inscriptions_evenement(membre_id); +CREATE INDEX idx_inscription_evenement ON inscriptions_evenement(evenement_id); +CREATE INDEX idx_inscription_date ON inscriptions_evenement(date_inscription); + +-- Index pour demandes_aide +CREATE INDEX idx_demande_demandeur ON demandes_aide(demandeur_id); +CREATE INDEX idx_demande_evaluateur ON demandes_aide(evaluateur_id); +CREATE INDEX idx_demande_organisation ON demandes_aide(organisation_id); +CREATE INDEX idx_demande_statut ON demandes_aide(statut); +CREATE INDEX idx_demande_type ON demandes_aide(type_aide); +CREATE INDEX idx_demande_date_demande ON demandes_aide(date_demande); + +-- ============================================ +-- ÉTAPE 6: Commentaires sur les tables +-- ============================================ + +COMMENT ON TABLE organisations IS 'Table des organisations (Lions Clubs, Associations, Coopératives, etc.) avec UUID'; +COMMENT ON TABLE membres IS 'Table des membres avec UUID'; +COMMENT ON TABLE cotisations IS 'Table des cotisations avec UUID'; +COMMENT ON TABLE evenements IS 'Table des événements avec UUID'; +COMMENT ON TABLE inscriptions_evenement IS 'Table des inscriptions aux événements avec UUID'; +COMMENT ON TABLE demandes_aide IS 'Table des demandes d''aide avec UUID'; + +COMMENT ON COLUMN organisations.id IS 'UUID unique de l''organisation'; +COMMENT ON COLUMN membres.id IS 'UUID unique du membre'; +COMMENT ON COLUMN cotisations.id IS 'UUID unique de la cotisation'; +COMMENT ON COLUMN evenements.id IS 'UUID unique de l''événement'; +COMMENT ON COLUMN inscriptions_evenement.id IS 'UUID unique de l''inscription'; +COMMENT ON COLUMN demandes_aide.id IS 'UUID unique de la demande d''aide'; + diff --git a/src/main/resources/import.sql b/src/main/resources/import.sql new file mode 100644 index 0000000..8b3c0d8 --- /dev/null +++ b/src/main/resources/import.sql @@ -0,0 +1,10 @@ +-- Script d'insertion de données initiales pour UnionFlow +-- Ce fichier est exécuté automatiquement par Hibernate au démarrage +-- Utilisé uniquement en mode développement (quarkus.hibernate-orm.database.generation=drop-and-create) +-- +-- IMPORTANT: Ce fichier ne doit PAS contenir de données fictives pour la production. +-- Les données doivent être insérées manuellement via l'interface d'administration +-- ou via des scripts de migration Flyway si nécessaire. +-- +-- Ce fichier est laissé vide intentionnellement pour éviter l'insertion automatique +-- de données fictives lors du démarrage du serveur. diff --git a/src/main/resources/keycloak/unionflow-realm.json b/src/main/resources/keycloak/unionflow-realm.json new file mode 100644 index 0000000..218ff11 --- /dev/null +++ b/src/main/resources/keycloak/unionflow-realm.json @@ -0,0 +1,307 @@ +{ + "realm": "unionflow", + "displayName": "UnionFlow", + "displayNameHtml": "

UnionFlow
", + "enabled": true, + "sslRequired": "external", + "registrationAllowed": true, + "registrationEmailAsUsername": true, + "rememberMe": true, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": false, + "bruteForceProtected": true, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "defaultRoles": ["offline_access", "uma_authorization", "default-roles-unionflow"], + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "supportedLocales": ["fr", "en"], + "defaultLocale": "fr", + "internationalizationEnabled": true, + "clients": [ + { + "clientId": "unionflow-server", + "name": "UnionFlow Server API", + "description": "Client pour l'API serveur UnionFlow", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "dev-secret", + "redirectUris": ["http://localhost:8080/*"], + "webOrigins": ["http://localhost:8080", "http://localhost:3000"], + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "name": "given_name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "name": "family_name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "name": "roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ], + "defaultClientScopes": ["web-origins", "role_list", "profile", "roles", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "clientId": "unionflow-mobile", + "name": "UnionFlow Mobile App", + "description": "Client pour l'application mobile UnionFlow", + "enabled": true, + "publicClient": true, + "redirectUris": ["unionflow://callback", "http://localhost:3000/callback"], + "webOrigins": ["*"], + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "fullScopeAllowed": true, + "defaultClientScopes": ["web-origins", "role_list", "profile", "roles", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + } + ], + "roles": { + "realm": [ + { + "name": "ADMIN", + "description": "Administrateur système avec tous les droits", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "PRESIDENT", + "description": "Président de l'union avec droits de gestion complète", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "SECRETAIRE", + "description": "Secrétaire avec droits de gestion des membres et événements", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "TRESORIER", + "description": "Trésorier avec droits de gestion financière", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "GESTIONNAIRE_MEMBRE", + "description": "Gestionnaire des membres avec droits de CRUD sur les membres", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "ORGANISATEUR_EVENEMENT", + "description": "Organisateur d'événements avec droits de gestion des événements", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "MEMBRE", + "description": "Membre standard avec droits de consultation", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + } + ] + }, + "users": [ + { + "username": "admin", + "enabled": true, + "emailVerified": true, + "firstName": "Administrateur", + "lastName": "Système", + "email": "admin@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "admin123", + "temporary": false + } + ], + "realmRoles": ["ADMIN", "PRESIDENT"], + "clientRoles": {} + }, + { + "username": "president", + "enabled": true, + "emailVerified": true, + "firstName": "Jean", + "lastName": "Dupont", + "email": "president@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "president123", + "temporary": false + } + ], + "realmRoles": ["PRESIDENT", "MEMBRE"], + "clientRoles": {} + }, + { + "username": "secretaire", + "enabled": true, + "emailVerified": true, + "firstName": "Marie", + "lastName": "Martin", + "email": "secretaire@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "secretaire123", + "temporary": false + } + ], + "realmRoles": ["SECRETAIRE", "GESTIONNAIRE_MEMBRE", "MEMBRE"], + "clientRoles": {} + }, + { + "username": "tresorier", + "enabled": true, + "emailVerified": true, + "firstName": "Pierre", + "lastName": "Durand", + "email": "tresorier@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "tresorier123", + "temporary": false + } + ], + "realmRoles": ["TRESORIER", "MEMBRE"], + "clientRoles": {} + }, + { + "username": "membre1", + "enabled": true, + "emailVerified": true, + "firstName": "Sophie", + "lastName": "Bernard", + "email": "membre1@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "membre123", + "temporary": false + } + ], + "realmRoles": ["MEMBRE"], + "clientRoles": {} + } + ], + "groups": [ + { + "name": "Administration", + "path": "/Administration", + "realmRoles": ["ADMIN"], + "subGroups": [] + }, + { + "name": "Bureau", + "path": "/Bureau", + "realmRoles": ["PRESIDENT", "SECRETAIRE", "TRESORIER"], + "subGroups": [] + }, + { + "name": "Gestionnaires", + "path": "/Gestionnaires", + "realmRoles": ["GESTIONNAIRE_MEMBRE", "ORGANISATEUR_EVENEMENT"], + "subGroups": [] + }, + { + "name": "Membres", + "path": "/Membres", + "realmRoles": ["MEMBRE"], + "subGroups": [] + } + ] +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/UnionFlowServerApplicationTest.java b/src/test.bak/java/dev/lions/unionflow/server/UnionFlowServerApplicationTest.java new file mode 100644 index 0000000..1926bf7 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/UnionFlowServerApplicationTest.java @@ -0,0 +1,155 @@ +package dev.lions.unionflow.server; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Tests pour UnionFlowServerApplication + * + * @author Lions Dev Team + * @since 2025-01-10 + */ +@QuarkusTest +@DisplayName("Tests UnionFlowServerApplication") +class UnionFlowServerApplicationTest { + + @Test + @DisplayName("Test de l'application - Contexte Quarkus") + void testApplicationContext() { + // Given & When & Then + // Le simple fait que ce test s'exécute sans erreur + // prouve que l'application Quarkus démarre correctement + assertThat(true).isTrue(); + } + + @Test + @DisplayName("Test de l'application - Classe principale existe") + void testMainClassExists() { + // Given & When & Then + assertThat(UnionFlowServerApplication.class).isNotNull(); + assertThat( + UnionFlowServerApplication.class.getAnnotation( + io.quarkus.runtime.annotations.QuarkusMain.class)) + .isNotNull(); + } + + @Test + @DisplayName("Test de l'application - Implémente QuarkusApplication") + void testImplementsQuarkusApplication() { + // Given & When & Then + assertThat(io.quarkus.runtime.QuarkusApplication.class) + .isAssignableFrom(UnionFlowServerApplication.class); + } + + @Test + @DisplayName("Test de l'application - Méthode main existe") + void testMainMethodExists() throws NoSuchMethodException { + // Given & When & Then + assertThat(UnionFlowServerApplication.class.getMethod("main", String[].class)).isNotNull(); + } + + @Test + @DisplayName("Test de l'application - Méthode run existe") + void testRunMethodExists() throws NoSuchMethodException { + // Given & When & Then + assertThat(UnionFlowServerApplication.class.getMethod("run", String[].class)).isNotNull(); + } + + @Test + @DisplayName("Test de l'application - Annotation ApplicationScoped") + void testApplicationScopedAnnotation() { + // Given & When & Then + assertThat( + UnionFlowServerApplication.class.getAnnotation( + jakarta.enterprise.context.ApplicationScoped.class)) + .isNotNull(); + } + + @Test + @DisplayName("Test de l'application - Logger statique") + void testStaticLogger() throws NoSuchFieldException { + // Given & When & Then + assertThat(UnionFlowServerApplication.class.getDeclaredField("LOG")).isNotNull(); + } + + @Test + @DisplayName("Test de l'application - Instance créable") + void testInstanceCreation() { + // Given & When + UnionFlowServerApplication app = new UnionFlowServerApplication(); + + // Then + assertThat(app).isNotNull(); + assertThat(app).isInstanceOf(io.quarkus.runtime.QuarkusApplication.class); + } + + @Test + @DisplayName("Test de la méthode main - Signature correcte") + void testMainMethodSignature() throws NoSuchMethodException { + // Given & When + var mainMethod = UnionFlowServerApplication.class.getMethod("main", String[].class); + + // Then + assertThat(mainMethod.getReturnType()).isEqualTo(void.class); + assertThat(java.lang.reflect.Modifier.isStatic(mainMethod.getModifiers())).isTrue(); + assertThat(java.lang.reflect.Modifier.isPublic(mainMethod.getModifiers())).isTrue(); + } + + @Test + @DisplayName("Test de la méthode run - Signature correcte") + void testRunMethodSignature() throws NoSuchMethodException { + // Given & When + var runMethod = UnionFlowServerApplication.class.getMethod("run", String[].class); + + // Then + assertThat(runMethod.getReturnType()).isEqualTo(int.class); + assertThat(java.lang.reflect.Modifier.isPublic(runMethod.getModifiers())).isTrue(); + assertThat(runMethod.getExceptionTypes()).contains(Exception.class); + } + + @Test + @DisplayName("Test de l'implémentation QuarkusApplication") + void testQuarkusApplicationImplementation() { + // Given & When & Then + assertThat( + io.quarkus.runtime.QuarkusApplication.class.isAssignableFrom( + UnionFlowServerApplication.class)) + .isTrue(); + } + + @Test + @DisplayName("Test du package de la classe") + void testPackageName() { + // Given & When & Then + assertThat(UnionFlowServerApplication.class.getPackage().getName()) + .isEqualTo("dev.lions.unionflow.server"); + } + + @Test + @DisplayName("Test de la classe - Modificateurs") + void testClassModifiers() { + // Given & When & Then + assertThat(java.lang.reflect.Modifier.isPublic(UnionFlowServerApplication.class.getModifiers())) + .isTrue(); + assertThat(java.lang.reflect.Modifier.isFinal(UnionFlowServerApplication.class.getModifiers())) + .isFalse(); + assertThat( + java.lang.reflect.Modifier.isAbstract(UnionFlowServerApplication.class.getModifiers())) + .isFalse(); + } + + @Test + @DisplayName("Test des constructeurs") + void testConstructors() { + // Given & When + var constructors = UnionFlowServerApplication.class.getConstructors(); + + // Then + assertThat(constructors).hasSize(1); + assertThat(constructors[0].getParameterCount()).isEqualTo(0); + assertThat(java.lang.reflect.Modifier.isPublic(constructors[0].getModifiers())).isTrue(); + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/entity/MembreSimpleTest.java b/src/test.bak/java/dev/lions/unionflow/server/entity/MembreSimpleTest.java new file mode 100644 index 0000000..d407bc4 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/entity/MembreSimpleTest.java @@ -0,0 +1,237 @@ +package dev.lions.unionflow.server.entity; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Tests simples pour l'entité Membre + * + * @author Lions Dev Team + * @since 2025-01-10 + */ +@DisplayName("Tests simples Membre") +class MembreSimpleTest { + + @Test + @DisplayName("Test de création d'un membre avec builder") + void testCreationMembreAvecBuilder() { + // Given & When + Membre membre = + Membre.builder() + .numeroMembre("UF2025-TEST01") + .prenom("Jean") + .nom("Dupont") + .email("jean.dupont@test.com") + .telephone("221701234567") + .dateNaissance(LocalDate.of(1990, 5, 15)) + .dateAdhesion(LocalDate.now()) + .actif(true) + .build(); + + // Then + assertThat(membre).isNotNull(); + assertThat(membre.getNumeroMembre()).isEqualTo("UF2025-TEST01"); + assertThat(membre.getPrenom()).isEqualTo("Jean"); + assertThat(membre.getNom()).isEqualTo("Dupont"); + assertThat(membre.getEmail()).isEqualTo("jean.dupont@test.com"); + assertThat(membre.getTelephone()).isEqualTo("221701234567"); + assertThat(membre.getDateNaissance()).isEqualTo(LocalDate.of(1990, 5, 15)); + assertThat(membre.getActif()).isTrue(); + } + + @Test + @DisplayName("Test de la méthode getNomComplet") + void testGetNomComplet() { + // Given + Membre membre = Membre.builder().prenom("Jean").nom("Dupont").build(); + + // When + String nomComplet = membre.getNomComplet(); + + // Then + assertThat(nomComplet).isEqualTo("Jean Dupont"); + } + + @Test + @DisplayName("Test de la méthode isMajeur - Majeur") + void testIsMajeurMajeur() { + // Given + Membre membre = Membre.builder().dateNaissance(LocalDate.of(1990, 5, 15)).build(); + + // When + boolean majeur = membre.isMajeur(); + + // Then + assertThat(majeur).isTrue(); + } + + @Test + @DisplayName("Test de la méthode isMajeur - Mineur") + void testIsMajeurMineur() { + // Given + Membre membre = Membre.builder().dateNaissance(LocalDate.now().minusYears(17)).build(); + + // When + boolean majeur = membre.isMajeur(); + + // Then + assertThat(majeur).isFalse(); + } + + @Test + @DisplayName("Test de la méthode getAge") + void testGetAge() { + // Given + Membre membre = Membre.builder().dateNaissance(LocalDate.now().minusYears(25)).build(); + + // When + int age = membre.getAge(); + + // Then + assertThat(age).isEqualTo(25); + } + + @Test + @DisplayName("Test de création d'un membre sans builder") + void testCreationMembreSansBuilder() { + // Given & When + Membre membre = new Membre(); + membre.setNumeroMembre("UF2025-TEST02"); + membre.setPrenom("Marie"); + membre.setNom("Martin"); + membre.setEmail("marie.martin@test.com"); + membre.setActif(true); + + // Then + assertThat(membre).isNotNull(); + assertThat(membre.getNumeroMembre()).isEqualTo("UF2025-TEST02"); + assertThat(membre.getPrenom()).isEqualTo("Marie"); + assertThat(membre.getNom()).isEqualTo("Martin"); + assertThat(membre.getEmail()).isEqualTo("marie.martin@test.com"); + assertThat(membre.getActif()).isTrue(); + } + + @Test + @DisplayName("Test des annotations JPA") + void testAnnotationsJPA() { + // Given & When & Then + assertThat(Membre.class.getAnnotation(jakarta.persistence.Entity.class)).isNotNull(); + assertThat(Membre.class.getAnnotation(jakarta.persistence.Table.class)).isNotNull(); + assertThat(Membre.class.getAnnotation(jakarta.persistence.Table.class).name()) + .isEqualTo("membres"); + } + + @Test + @DisplayName("Test des annotations Lombok") + void testAnnotationsLombok() { + // Given & When & Then + // Vérifier que les annotations Lombok sont présentes (peuvent être null selon la compilation) + // Nous testons plutôt que les méthodes générées existent + assertThat(Membre.builder()).isNotNull(); + + Membre membre = new Membre(); + assertThat(membre.toString()).isNotNull(); + assertThat(membre.hashCode()).isNotZero(); + } + + @Test + @DisplayName("Test de l'héritage PanacheEntity") + void testHeritageePanacheEntity() { + // Given & When & Then + assertThat(io.quarkus.hibernate.orm.panache.PanacheEntity.class).isAssignableFrom(Membre.class); + } + + @Test + @DisplayName("Test des méthodes héritées de PanacheEntity") + void testMethodesHeriteesPanacheEntity() throws NoSuchMethodException { + // Given & When & Then + // Vérifier que les méthodes de PanacheEntity sont disponibles + assertThat(Membre.class.getMethod("persist")).isNotNull(); + assertThat(Membre.class.getMethod("delete")).isNotNull(); + assertThat(Membre.class.getMethod("isPersistent")).isNotNull(); + } + + @Test + @DisplayName("Test de toString") + void testToString() { + // Given + Membre membre = + Membre.builder() + .numeroMembre("UF2025-TEST01") + .prenom("Jean") + .nom("Dupont") + .email("jean.dupont@test.com") + .actif(true) + .build(); + + // When + String toString = membre.toString(); + + // Then + assertThat(toString).isNotNull(); + assertThat(toString).contains("Jean"); + assertThat(toString).contains("Dupont"); + assertThat(toString).contains("UF2025-TEST01"); + assertThat(toString).contains("jean.dupont@test.com"); + } + + @Test + @DisplayName("Test de hashCode") + void testHashCode() { + // Given + Membre membre1 = + Membre.builder() + .numeroMembre("UF2025-TEST01") + .prenom("Jean") + .nom("Dupont") + .email("jean.dupont@test.com") + .build(); + + Membre membre2 = + Membre.builder() + .numeroMembre("UF2025-TEST01") + .prenom("Jean") + .nom("Dupont") + .email("jean.dupont@test.com") + .build(); + + // When & Then + assertThat(membre1.hashCode()).isNotZero(); + assertThat(membre2.hashCode()).isNotZero(); + } + + @Test + @DisplayName("Test des propriétés nulles") + void testProprietesNulles() { + // Given + Membre membre = new Membre(); + + // When & Then + assertThat(membre.getNumeroMembre()).isNull(); + assertThat(membre.getPrenom()).isNull(); + assertThat(membre.getNom()).isNull(); + assertThat(membre.getEmail()).isNull(); + assertThat(membre.getTelephone()).isNull(); + assertThat(membre.getDateNaissance()).isNull(); + assertThat(membre.getDateAdhesion()).isNull(); + // Le champ actif a une valeur par défaut à true dans l'entité + // assertThat(membre.getActif()).isNull(); + } + + @Test + @DisplayName("Test de la méthode preUpdate") + void testPreUpdate() { + // Given + Membre membre = new Membre(); + assertThat(membre.getDateModification()).isNull(); + + // When + membre.preUpdate(); + + // Then + assertThat(membre.getDateModification()).isNotNull(); + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/repository/MembreRepositoryIntegrationTest.java b/src/test.bak/java/dev/lions/unionflow/server/repository/MembreRepositoryIntegrationTest.java new file mode 100644 index 0000000..7ae9eff --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/repository/MembreRepositoryIntegrationTest.java @@ -0,0 +1,184 @@ +package dev.lions.unionflow.server.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.lions.unionflow.server.entity.Membre; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Tests d'intégration pour MembreRepository + * + * @author Lions Dev Team + * @since 2025-01-10 + */ +@QuarkusTest +@DisplayName("Tests d'intégration MembreRepository") +class MembreRepositoryIntegrationTest { + + @Inject MembreRepository membreRepository; + + private Membre membreTest; + + @BeforeEach + @Transactional + void setUp() { + // Nettoyer la base de données + membreRepository.deleteAll(); + + // Créer un membre de test + membreTest = + Membre.builder() + .numeroMembre("UF2025-TEST01") + .prenom("Jean") + .nom("Dupont") + .email("jean.dupont@test.com") + .telephone("221701234567") + .dateNaissance(LocalDate.of(1990, 5, 15)) + .dateAdhesion(LocalDate.now()) + .actif(true) + .build(); + + membreRepository.persist(membreTest); + } + + @Test + @DisplayName("Test findByEmail - Membre existant") + @Transactional + void testFindByEmailExistant() { + // When + Optional result = membreRepository.findByEmail("jean.dupont@test.com"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getPrenom()).isEqualTo("Jean"); + assertThat(result.get().getNom()).isEqualTo("Dupont"); + } + + @Test + @DisplayName("Test findByEmail - Membre inexistant") + @Transactional + void testFindByEmailInexistant() { + // When + Optional result = membreRepository.findByEmail("inexistant@test.com"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Test findByNumeroMembre - Membre existant") + @Transactional + void testFindByNumeroMembreExistant() { + // When + Optional result = membreRepository.findByNumeroMembre("UF2025-TEST01"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getPrenom()).isEqualTo("Jean"); + assertThat(result.get().getNom()).isEqualTo("Dupont"); + } + + @Test + @DisplayName("Test findByNumeroMembre - Membre inexistant") + @Transactional + void testFindByNumeroMembreInexistant() { + // When + Optional result = membreRepository.findByNumeroMembre("UF2025-INEXISTANT"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Test findAllActifs - Seuls les membres actifs") + @Transactional + void testFindAllActifs() { + // Given - Ajouter un membre inactif + Membre membreInactif = + Membre.builder() + .numeroMembre("UF2025-TEST02") + .prenom("Marie") + .nom("Martin") + .email("marie.martin@test.com") + .telephone("221701234568") + .dateNaissance(LocalDate.of(1985, 8, 20)) + .dateAdhesion(LocalDate.now()) + .actif(false) + .build(); + membreRepository.persist(membreInactif); + + // When + List result = membreRepository.findAllActifs(); + + // Then + assertThat(result).hasSize(1); + assertThat(result.get(0).getActif()).isTrue(); + assertThat(result.get(0).getPrenom()).isEqualTo("Jean"); + } + + @Test + @DisplayName("Test countActifs - Nombre de membres actifs") + @Transactional + void testCountActifs() { + // When + long count = membreRepository.countActifs(); + + // Then + assertThat(count).isEqualTo(1); + } + + @Test + @DisplayName("Test findByNomOrPrenom - Recherche par nom") + @Transactional + void testFindByNomOrPrenomParNom() { + // When + List result = membreRepository.findByNomOrPrenom("dupont"); + + // Then + assertThat(result).hasSize(1); + assertThat(result.get(0).getNom()).isEqualTo("Dupont"); + } + + @Test + @DisplayName("Test findByNomOrPrenom - Recherche par prénom") + @Transactional + void testFindByNomOrPrenomParPrenom() { + // When + List result = membreRepository.findByNomOrPrenom("jean"); + + // Then + assertThat(result).hasSize(1); + assertThat(result.get(0).getPrenom()).isEqualTo("Jean"); + } + + @Test + @DisplayName("Test findByNomOrPrenom - Aucun résultat") + @Transactional + void testFindByNomOrPrenomAucunResultat() { + // When + List result = membreRepository.findByNomOrPrenom("inexistant"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Test findByNomOrPrenom - Recherche insensible à la casse") + @Transactional + void testFindByNomOrPrenomCaseInsensitive() { + // When + List result = membreRepository.findByNomOrPrenom("DUPONT"); + + // Then + assertThat(result).hasSize(1); + assertThat(result.get(0).getNom()).isEqualTo("Dupont"); + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/repository/MembreRepositoryTest.java b/src/test.bak/java/dev/lions/unionflow/server/repository/MembreRepositoryTest.java new file mode 100644 index 0000000..d45e356 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/repository/MembreRepositoryTest.java @@ -0,0 +1,105 @@ +package dev.lions.unionflow.server.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.lions.unionflow.server.entity.Membre; +import java.time.LocalDate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests pour MembreRepository + * + * @author Lions Dev Team + * @since 2025-01-10 + */ +@ExtendWith(MockitoExtension.class) +@DisplayName("Tests MembreRepository") +class MembreRepositoryTest { + + @Mock MembreRepository membreRepository; + + private Membre membreTest; + private Membre membreInactif; + + @BeforeEach + void setUp() { + + // Créer des membres de test + membreTest = + Membre.builder() + .numeroMembre("UF2025-TEST01") + .prenom("Jean") + .nom("Dupont") + .email("jean.dupont@test.com") + .telephone("221701234567") + .dateNaissance(LocalDate.of(1990, 5, 15)) + .dateAdhesion(LocalDate.now()) + .actif(true) + .build(); + + membreInactif = + Membre.builder() + .numeroMembre("UF2025-TEST02") + .prenom("Marie") + .nom("Martin") + .email("marie.martin@test.com") + .telephone("221701234568") + .dateNaissance(LocalDate.of(1985, 8, 20)) + .dateAdhesion(LocalDate.now()) + .actif(false) + .build(); + } + + @Test + @DisplayName("Test de l'existence de la classe MembreRepository") + void testMembreRepositoryExists() { + // Given & When & Then + assertThat(MembreRepository.class).isNotNull(); + assertThat(membreRepository).isNotNull(); + } + + @Test + @DisplayName("Test des méthodes du repository") + void testRepositoryMethods() throws NoSuchMethodException { + // Given & When & Then + assertThat(MembreRepository.class.getMethod("findByEmail", String.class)).isNotNull(); + assertThat(MembreRepository.class.getMethod("findByNumeroMembre", String.class)).isNotNull(); + assertThat(MembreRepository.class.getMethod("findAllActifs")).isNotNull(); + assertThat(MembreRepository.class.getMethod("countActifs")).isNotNull(); + assertThat(MembreRepository.class.getMethod("findByNomOrPrenom", String.class)).isNotNull(); + } + + @Test + @DisplayName("Test de l'annotation ApplicationScoped") + void testApplicationScopedAnnotation() { + // Given & When & Then + assertThat( + MembreRepository.class.getAnnotation( + jakarta.enterprise.context.ApplicationScoped.class)) + .isNotNull(); + } + + @Test + @DisplayName("Test de l'implémentation PanacheRepository") + void testPanacheRepositoryImplementation() { + // Given & When & Then + assertThat(io.quarkus.hibernate.orm.panache.PanacheRepository.class) + .isAssignableFrom(MembreRepository.class); + } + + @Test + @DisplayName("Test de la création d'instance") + void testInstanceCreation() { + // Given & When + MembreRepository repository = new MembreRepository(); + + // Then + assertThat(repository).isNotNull(); + assertThat(repository).isInstanceOf(io.quarkus.hibernate.orm.panache.PanacheRepository.class); + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/resource/AideResourceTest.java b/src/test.bak/java/dev/lions/unionflow/server/resource/AideResourceTest.java new file mode 100644 index 0000000..ba0d28e --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/resource/AideResourceTest.java @@ -0,0 +1,394 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import dev.lions.unionflow.server.api.dto.solidarite.aide.AideDTO; +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.service.AideService; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +/** + * Tests d'intégration pour AideResource + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@QuarkusTest +@DisplayName("AideResource - Tests d'intégration") +class AideResourceTest { + + @Mock AideService aideService; + + private AideDTO aideDTOTest; + private List listeAidesTest; + + @BeforeEach + void setUp() { + // DTO de test + aideDTOTest = new AideDTO(); + aideDTOTest.setId(UUID.randomUUID()); + aideDTOTest.setNumeroReference("AIDE-2025-TEST01"); + aideDTOTest.setTitre("Aide médicale urgente"); + aideDTOTest.setDescription("Demande d'aide pour frais médicaux urgents"); + aideDTOTest.setTypeAide("MEDICALE"); + aideDTOTest.setMontantDemande(new BigDecimal("500000.00")); + aideDTOTest.setStatut("EN_ATTENTE"); + aideDTOTest.setPriorite("URGENTE"); + aideDTOTest.setMembreDemandeurId(UUID.randomUUID()); + aideDTOTest.setAssociationId(UUID.randomUUID()); + aideDTOTest.setActif(true); + + // Liste de test + listeAidesTest = Arrays.asList(aideDTOTest); + } + + @Nested + @DisplayName("Tests des endpoints CRUD") + class CrudEndpointsTests { + + @Test + @TestSecurity( + user = "admin", + roles = {"admin"}) + @DisplayName("GET /api/aides - Liste des aides") + void testListerAides() { + // Given + when(aideService.listerAidesActives(0, 20)).thenReturn(listeAidesTest); + + // When & Then + given() + .when() + .get("/api/aides") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", is(1)) + .body("[0].titre", equalTo("Aide médicale urgente")) + .body("[0].statut", equalTo("EN_ATTENTE")); + } + + @Test + @TestSecurity( + user = "admin", + roles = {"admin"}) + @DisplayName("GET /api/aides/{id} - Récupération par ID") + void testObtenirAideParId() { + // Given + when(aideService.obtenirAideParId(1L)).thenReturn(aideDTOTest); + + // When & Then + given() + .when() + .get("/api/aides/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("titre", equalTo("Aide médicale urgente")) + .body("numeroReference", equalTo("AIDE-2025-TEST01")); + } + + @Test + @TestSecurity( + user = "admin", + roles = {"admin"}) + @DisplayName("GET /api/aides/{id} - Aide non trouvée") + void testObtenirAideParId_NonTrouvee() { + // Given + when(aideService.obtenirAideParId(999L)) + .thenThrow(new NotFoundException("Demande d'aide non trouvée")); + + // When & Then + given() + .when() + .get("/api/aides/999") + .then() + .statusCode(404) + .contentType(ContentType.JSON) + .body("error", equalTo("Demande d'aide non trouvée")); + } + + @Test + @TestSecurity( + user = "admin", + roles = {"admin"}) + @DisplayName("POST /api/aides - Création d'aide") + void testCreerAide() { + // Given + when(aideService.creerAide(any(AideDTO.class))).thenReturn(aideDTOTest); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(aideDTOTest) + .when() + .post("/api/aides") + .then() + .statusCode(201) + .contentType(ContentType.JSON) + .body("titre", equalTo("Aide médicale urgente")) + .body("numeroReference", equalTo("AIDE-2025-TEST01")); + } + + @Test + @TestSecurity( + user = "admin", + roles = {"admin"}) + @DisplayName("PUT /api/aides/{id} - Mise à jour d'aide") + void testMettreAJourAide() { + // Given + AideDTO aideMiseAJour = new AideDTO(); + aideMiseAJour.setTitre("Titre modifié"); + aideMiseAJour.setDescription("Description modifiée"); + + when(aideService.mettreAJourAide(eq(1L), any(AideDTO.class))).thenReturn(aideMiseAJour); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(aideMiseAJour) + .when() + .put("/api/aides/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("titre", equalTo("Titre modifié")); + } + } + + @Nested + @DisplayName("Tests des endpoints métier") + class EndpointsMetierTests { + + @Test + @TestSecurity( + user = "evaluateur", + roles = {"evaluateur_aide"}) + @DisplayName("POST /api/aides/{id}/approuver - Approbation d'aide") + void testApprouverAide() { + // Given + AideDTO aideApprouvee = new AideDTO(); + aideApprouvee.setStatut("APPROUVEE"); + aideApprouvee.setMontantApprouve(new BigDecimal("400000.00")); + + when(aideService.approuverAide(eq(1L), any(BigDecimal.class), anyString())) + .thenReturn(aideApprouvee); + + Map approbationData = + Map.of( + "montantApprouve", "400000.00", + "commentaires", "Aide approuvée après évaluation"); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(approbationData) + .when() + .post("/api/aides/1/approuver") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("statut", equalTo("APPROUVEE")); + } + + @Test + @TestSecurity( + user = "evaluateur", + roles = {"evaluateur_aide"}) + @DisplayName("POST /api/aides/{id}/rejeter - Rejet d'aide") + void testRejeterAide() { + // Given + AideDTO aideRejetee = new AideDTO(); + aideRejetee.setStatut("REJETEE"); + aideRejetee.setRaisonRejet("Dossier incomplet"); + + when(aideService.rejeterAide(eq(1L), anyString())).thenReturn(aideRejetee); + + Map rejetData = Map.of("raisonRejet", "Dossier incomplet"); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(rejetData) + .when() + .post("/api/aides/1/rejeter") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("statut", equalTo("REJETEE")); + } + + @Test + @TestSecurity( + user = "tresorier", + roles = {"tresorier"}) + @DisplayName("POST /api/aides/{id}/verser - Versement d'aide") + void testMarquerCommeVersee() { + // Given + AideDTO aideVersee = new AideDTO(); + aideVersee.setStatut("VERSEE"); + aideVersee.setMontantVerse(new BigDecimal("400000.00")); + + when(aideService.marquerCommeVersee(eq(1L), any(BigDecimal.class), anyString(), anyString())) + .thenReturn(aideVersee); + + Map versementData = + Map.of( + "montantVerse", "400000.00", + "modeVersement", "MOBILE_MONEY", + "numeroTransaction", "TXN123456789"); + + // When & Then + given() + .contentType(ContentType.JSON) + .body(versementData) + .when() + .post("/api/aides/1/verser") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("statut", equalTo("VERSEE")); + } + } + + @Nested + @DisplayName("Tests des endpoints de recherche") + class EndpointsRechercheTests { + + @Test + @TestSecurity( + user = "membre", + roles = {"membre"}) + @DisplayName("GET /api/aides/statut/{statut} - Filtrage par statut") + void testListerAidesParStatut() { + // Given + when(aideService.listerAidesParStatut(StatutAide.EN_ATTENTE, 0, 20)) + .thenReturn(listeAidesTest); + + // When & Then + given() + .when() + .get("/api/aides/statut/EN_ATTENTE") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", is(1)) + .body("[0].statut", equalTo("EN_ATTENTE")); + } + + @Test + @TestSecurity( + user = "membre", + roles = {"membre"}) + @DisplayName("GET /api/aides/membre/{membreId} - Aides d'un membre") + void testListerAidesParMembre() { + // Given + when(aideService.listerAidesParMembre(1L, 0, 20)).thenReturn(listeAidesTest); + + // When & Then + given() + .when() + .get("/api/aides/membre/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", is(1)); + } + + @Test + @TestSecurity( + user = "membre", + roles = {"membre"}) + @DisplayName("GET /api/aides/recherche - Recherche textuelle") + void testRechercherAides() { + // Given + when(aideService.rechercherAides("médical", 0, 20)).thenReturn(listeAidesTest); + + // When & Then + given() + .queryParam("q", "médical") + .when() + .get("/api/aides/recherche") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", is(1)); + } + + @Test + @TestSecurity( + user = "admin", + roles = {"admin"}) + @DisplayName("GET /api/aides/statistiques - Statistiques") + void testObtenirStatistiques() { + // Given + Map statistiques = + Map.of( + "total", 100L, + "enAttente", 25L, + "approuvees", 50L, + "versees", 20L); + when(aideService.obtenirStatistiquesGlobales()).thenReturn(statistiques); + + // When & Then + given() + .when() + .get("/api/aides/statistiques") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("total", equalTo(100)) + .body("enAttente", equalTo(25)) + .body("approuvees", equalTo(50)) + .body("versees", equalTo(20)); + } + } + + @Nested + @DisplayName("Tests de sécurité") + class SecurityTests { + + @Test + @DisplayName("Accès non authentifié - 401") + void testAccesNonAuthentifie() { + given().when().get("/api/aides").then().statusCode(401); + } + + @Test + @TestSecurity( + user = "membre", + roles = {"membre"}) + @DisplayName("Accès non autorisé pour approbation - 403") + void testAccesNonAutorisePourApprobation() { + Map approbationData = + Map.of( + "montantApprouve", "400000.00", + "commentaires", "Test"); + + given() + .contentType(ContentType.JSON) + .body(approbationData) + .when() + .post("/api/aides/1/approuver") + .then() + .statusCode(403); + } + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/resource/CotisationResourceTest.java b/src/test.bak/java/dev/lions/unionflow/server/resource/CotisationResourceTest.java new file mode 100644 index 0000000..68e14ff --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/resource/CotisationResourceTest.java @@ -0,0 +1,325 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +import dev.lions.unionflow.server.api.dto.finance.CotisationDTO; +import dev.lions.unionflow.server.entity.Cotisation; +import dev.lions.unionflow.server.entity.Membre; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +/** + * Tests d'intégration pour CotisationResource Teste tous les endpoints REST de l'API cotisations + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@DisplayName("Tests d'intégration - API Cotisations") +class CotisationResourceTest { + + private static Long membreTestId; + private static Long cotisationTestId; + private static String numeroReferenceTest; + + @BeforeEach + @Transactional + void setUp() { + // Nettoyage et création des données de test + Cotisation.deleteAll(); + Membre.deleteAll(); + + // Création d'un membre de test + Membre membreTest = new Membre(); + membreTest.setNumeroMembre("MBR-TEST-001"); + membreTest.setNom("Dupont"); + membreTest.setPrenom("Jean"); + membreTest.setEmail("jean.dupont@test.com"); + membreTest.setTelephone("+225070123456"); + membreTest.setDateNaissance(LocalDate.of(1985, 5, 15)); + membreTest.setActif(true); + membreTest.persist(); + + membreTestId = membreTest.id; + } + + @Test + @org.junit.jupiter.api.Order(1) + @DisplayName("POST /api/cotisations - Création d'une cotisation") + void testCreateCotisation() { + CotisationDTO nouvelleCotisation = new CotisationDTO(); + nouvelleCotisation.setMembreId(UUID.fromString(membreTestId.toString())); + nouvelleCotisation.setTypeCotisation("MENSUELLE"); + nouvelleCotisation.setMontantDu(new BigDecimal("25000.00")); + nouvelleCotisation.setDateEcheance(LocalDate.now().plusDays(30)); + nouvelleCotisation.setDescription("Cotisation mensuelle janvier 2025"); + nouvelleCotisation.setPeriode("Janvier 2025"); + nouvelleCotisation.setAnnee(2025); + nouvelleCotisation.setMois(1); + + given() + .contentType(ContentType.JSON) + .body(nouvelleCotisation) + .when() + .post("/api/cotisations") + .then() + .statusCode(201) + .body("numeroReference", notNullValue()) + .body("membreId", equalTo(membreTestId.toString())) + .body("typeCotisation", equalTo("MENSUELLE")) + .body("montantDu", equalTo(25000.00f)) + .body("montantPaye", equalTo(0.0f)) + .body("statut", equalTo("EN_ATTENTE")) + .body("codeDevise", equalTo("XOF")) + .body("annee", equalTo(2025)) + .body("mois", equalTo(1)); + } + + @Test + @org.junit.jupiter.api.Order(2) + @DisplayName("GET /api/cotisations - Liste des cotisations") + void testGetAllCotisations() { + given() + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .get("/api/cotisations") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @org.junit.jupiter.api.Order(3) + @DisplayName("GET /api/cotisations/{id} - Récupération par ID") + void testGetCotisationById() { + // Créer d'abord une cotisation + CotisationDTO cotisation = createTestCotisation(); + cotisationTestId = Long.valueOf(cotisation.getId().toString()); + + given() + .pathParam("id", cotisationTestId) + .when() + .get("/api/cotisations/{id}") + .then() + .statusCode(200) + .body("id", equalTo(cotisationTestId.toString())) + .body("typeCotisation", equalTo("MENSUELLE")); + } + + @Test + @org.junit.jupiter.api.Order(4) + @DisplayName("GET /api/cotisations/reference/{numeroReference} - Récupération par référence") + void testGetCotisationByReference() { + // Utiliser la cotisation créée précédemment + if (numeroReferenceTest == null) { + CotisationDTO cotisation = createTestCotisation(); + numeroReferenceTest = cotisation.getNumeroReference(); + } + + given() + .pathParam("numeroReference", numeroReferenceTest) + .when() + .get("/api/cotisations/reference/{numeroReference}") + .then() + .statusCode(200) + .body("numeroReference", equalTo(numeroReferenceTest)) + .body("typeCotisation", equalTo("MENSUELLE")); + } + + @Test + @org.junit.jupiter.api.Order(5) + @DisplayName("PUT /api/cotisations/{id} - Mise à jour d'une cotisation") + void testUpdateCotisation() { + // Créer une cotisation si nécessaire + if (cotisationTestId == null) { + CotisationDTO cotisation = createTestCotisation(); + cotisationTestId = Long.valueOf(cotisation.getId().toString()); + } + + CotisationDTO cotisationMiseAJour = new CotisationDTO(); + cotisationMiseAJour.setTypeCotisation("TRIMESTRIELLE"); + cotisationMiseAJour.setMontantDu(new BigDecimal("75000.00")); + cotisationMiseAJour.setDescription("Cotisation trimestrielle Q1 2025"); + cotisationMiseAJour.setObservations("Mise à jour du type de cotisation"); + + given() + .contentType(ContentType.JSON) + .pathParam("id", cotisationTestId) + .body(cotisationMiseAJour) + .when() + .put("/api/cotisations/{id}") + .then() + .statusCode(200) + .body("typeCotisation", equalTo("TRIMESTRIELLE")) + .body("montantDu", equalTo(75000.00f)) + .body("observations", equalTo("Mise à jour du type de cotisation")); + } + + @Test + @org.junit.jupiter.api.Order(6) + @DisplayName("GET /api/cotisations/membre/{membreId} - Cotisations d'un membre") + void testGetCotisationsByMembre() { + given() + .pathParam("membreId", membreTestId) + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .get("/api/cotisations/membre/{membreId}") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @org.junit.jupiter.api.Order(7) + @DisplayName("GET /api/cotisations/statut/{statut} - Cotisations par statut") + void testGetCotisationsByStatut() { + given() + .pathParam("statut", "EN_ATTENTE") + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .get("/api/cotisations/statut/{statut}") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @org.junit.jupiter.api.Order(8) + @DisplayName("GET /api/cotisations/en-retard - Cotisations en retard") + void testGetCotisationsEnRetard() { + given() + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .get("/api/cotisations/en-retard") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @org.junit.jupiter.api.Order(9) + @DisplayName("GET /api/cotisations/recherche - Recherche avancée") + void testRechercherCotisations() { + given() + .queryParam("membreId", membreTestId) + .queryParam("statut", "EN_ATTENTE") + .queryParam("annee", 2025) + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .get("/api/cotisations/recherche") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @org.junit.jupiter.api.Order(10) + @DisplayName("GET /api/cotisations/stats - Statistiques des cotisations") + void testGetStatistiquesCotisations() { + given() + .when() + .get("/api/cotisations/stats") + .then() + .statusCode(200) + .body("totalCotisations", notNullValue()) + .body("cotisationsPayees", notNullValue()) + .body("cotisationsEnRetard", notNullValue()) + .body("tauxPaiement", notNullValue()); + } + + @Test + @org.junit.jupiter.api.Order(11) + @DisplayName("DELETE /api/cotisations/{id} - Suppression d'une cotisation") + void testDeleteCotisation() { + // Créer une cotisation si nécessaire + if (cotisationTestId == null) { + CotisationDTO cotisation = createTestCotisation(); + cotisationTestId = Long.valueOf(cotisation.getId().toString()); + } + + given() + .pathParam("id", cotisationTestId) + .when() + .delete("/api/cotisations/{id}") + .then() + .statusCode(204); + + // Vérifier que la cotisation est marquée comme annulée + given() + .pathParam("id", cotisationTestId) + .when() + .get("/api/cotisations/{id}") + .then() + .statusCode(200) + .body("statut", equalTo("ANNULEE")); + } + + @Test + @DisplayName("GET /api/cotisations/{id} - Cotisation inexistante") + void testGetCotisationByIdNotFound() { + given() + .pathParam("id", 99999L) + .when() + .get("/api/cotisations/{id}") + .then() + .statusCode(404) + .body("error", equalTo("Cotisation non trouvée")); + } + + @Test + @DisplayName("POST /api/cotisations - Données invalides") + void testCreateCotisationInvalidData() { + CotisationDTO cotisationInvalide = new CotisationDTO(); + // Données manquantes ou invalides + cotisationInvalide.setTypeCotisation(""); + cotisationInvalide.setMontantDu(new BigDecimal("-100")); + + given() + .contentType(ContentType.JSON) + .body(cotisationInvalide) + .when() + .post("/api/cotisations") + .then() + .statusCode(400); + } + + /** Méthode utilitaire pour créer une cotisation de test */ + private CotisationDTO createTestCotisation() { + CotisationDTO cotisation = new CotisationDTO(); + cotisation.setMembreId(UUID.fromString(membreTestId.toString())); + cotisation.setTypeCotisation("MENSUELLE"); + cotisation.setMontantDu(new BigDecimal("25000.00")); + cotisation.setDateEcheance(LocalDate.now().plusDays(30)); + cotisation.setDescription("Cotisation de test"); + cotisation.setPeriode("Test 2025"); + cotisation.setAnnee(2025); + cotisation.setMois(1); + + return given() + .contentType(ContentType.JSON) + .body(cotisation) + .when() + .post("/api/cotisations") + .then() + .statusCode(201) + .extract() + .as(CotisationDTO.class); + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/resource/EvenementResourceTest.java b/src/test.bak/java/dev/lions/unionflow/server/resource/EvenementResourceTest.java new file mode 100644 index 0000000..02f098d --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/resource/EvenementResourceTest.java @@ -0,0 +1,448 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +import dev.lions.unionflow.server.entity.Evenement; +import dev.lions.unionflow.server.entity.Evenement.StatutEvenement; +import dev.lions.unionflow.server.entity.Evenement.TypeEvenement; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import org.junit.jupiter.api.*; + +/** + * Tests d'intégration pour EvenementResource + * + *

Tests complets de l'API REST des événements avec authentification et validation des + * permissions. Optimisé pour l'intégration mobile. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@DisplayName("Tests d'intégration - API Événements") +class EvenementResourceTest { + + private static Long evenementTestId; + private static Long organisationTestId; + private static Long membreTestId; + + @BeforeAll + @Transactional + static void setupTestData() { + // Créer une organisation de test + Organisation organisation = + Organisation.builder() + .nom("Union Test API") + .typeOrganisation("ASSOCIATION") + .statut("ACTIVE") + .email("test-api@union.com") + .telephone("0123456789") + .adresse("123 Rue de Test") + .codePostal("75001") + .ville("Paris") + .pays("France") + .actif(true) + .creePar("test@unionflow.dev") + .dateCreation(LocalDateTime.now()) + .build(); + organisation.persist(); + organisationTestId = organisation.id; + + // Créer un membre de test + Membre membre = + Membre.builder() + .numeroMembre("UF2025-API01") + .prenom("Marie") + .nom("Martin") + .email("marie.martin@test.com") + .telephone("0987654321") + .dateNaissance(LocalDate.of(1990, 5, 15)) + .dateAdhesion(LocalDate.now()) + .actif(true) + .organisation(organisation) + .build(); + membre.persist(); + membreTestId = membre.id; + + // Créer un événement de test + Evenement evenement = + Evenement.builder() + .titre("Conférence API Test") + .description("Conférence de test pour l'API") + .dateDebut(LocalDateTime.now().plusDays(15)) + .dateFin(LocalDateTime.now().plusDays(15).plusHours(2)) + .lieu("Centre de conférence Test") + .typeEvenement(TypeEvenement.CONFERENCE) + .statut(StatutEvenement.PLANIFIE) + .capaciteMax(50) + .prix(BigDecimal.valueOf(15.00)) + .inscriptionRequise(true) + .visiblePublic(true) + .actif(true) + .organisation(organisation) + .organisateur(membre) + .creePar("test@unionflow.dev") + .dateCreation(LocalDateTime.now()) + .build(); + evenement.persist(); + evenementTestId = evenement.id; + } + + @Test + @Order(1) + @DisplayName("GET /api/evenements - Lister événements (authentifié)") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"MEMBRE"}) + void testListerEvenements_Authentifie() { + given() + .when() + .get("/api/evenements") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", greaterThanOrEqualTo(1)) + .body("[0].titre", notNullValue()) + .body("[0].dateDebut", notNullValue()) + .body("[0].statut", notNullValue()); + } + + @Test + @Order(2) + @DisplayName("GET /api/evenements - Non authentifié") + void testListerEvenements_NonAuthentifie() { + given().when().get("/api/evenements").then().statusCode(401); + } + + @Test + @Order(3) + @DisplayName("GET /api/evenements/{id} - Récupérer événement") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"MEMBRE"}) + void testObtenirEvenement() { + given() + .pathParam("id", evenementTestId) + .when() + .get("/api/evenements/{id}") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("id", equalTo(evenementTestId.intValue())) + .body("titre", equalTo("Conférence API Test")) + .body("description", equalTo("Conférence de test pour l'API")) + .body("typeEvenement", equalTo("CONFERENCE")) + .body("statut", equalTo("PLANIFIE")) + .body("capaciteMax", equalTo(50)) + .body("prix", equalTo(15.0f)) + .body("inscriptionRequise", equalTo(true)) + .body("visiblePublic", equalTo(true)) + .body("actif", equalTo(true)); + } + + @Test + @Order(4) + @DisplayName("GET /api/evenements/{id} - Événement non trouvé") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"MEMBRE"}) + void testObtenirEvenement_NonTrouve() { + given() + .pathParam("id", 99999) + .when() + .get("/api/evenements/{id}") + .then() + .statusCode(404) + .body("error", equalTo("Événement non trouvé")); + } + + @Test + @Order(5) + @DisplayName("POST /api/evenements - Créer événement (organisateur)") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"ORGANISATEUR_EVENEMENT"}) + void testCreerEvenement_Organisateur() { + String nouvelEvenement = + String.format( + """ + { + "titre": "Nouvel Événement Test", + "description": "Description du nouvel événement", + "dateDebut": "%s", + "dateFin": "%s", + "lieu": "Lieu de test", + "typeEvenement": "FORMATION", + "capaciteMax": 30, + "prix": 20.00, + "inscriptionRequise": true, + "visiblePublic": true, + "organisation": {"id": %d}, + "organisateur": {"id": %d} + } + """, + LocalDateTime.now().plusDays(20).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + LocalDateTime.now() + .plusDays(20) + .plusHours(3) + .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + organisationTestId, + membreTestId); + + given() + .contentType(ContentType.JSON) + .body(nouvelEvenement) + .when() + .post("/api/evenements") + .then() + .statusCode(201) + .contentType(ContentType.JSON) + .body("titre", equalTo("Nouvel Événement Test")) + .body("typeEvenement", equalTo("FORMATION")) + .body("capaciteMax", equalTo(30)) + .body("prix", equalTo(20.0f)) + .body("actif", equalTo(true)); + } + + @Test + @Order(6) + @DisplayName("POST /api/evenements - Permissions insuffisantes") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"MEMBRE"}) + void testCreerEvenement_PermissionsInsuffisantes() { + String nouvelEvenement = + """ + { + "titre": "Événement Non Autorisé", + "description": "Test permissions", + "dateDebut": "2025-02-15T10:00:00", + "dateFin": "2025-02-15T12:00:00", + "lieu": "Lieu test", + "typeEvenement": "FORMATION" + } + """; + + given() + .contentType(ContentType.JSON) + .body(nouvelEvenement) + .when() + .post("/api/evenements") + .then() + .statusCode(403); + } + + @Test + @Order(7) + @DisplayName("PUT /api/evenements/{id} - Mettre à jour événement") + @TestSecurity( + user = "admin@unionflow.dev", + roles = {"ADMIN"}) + void testMettreAJourEvenement_Admin() { + String evenementModifie = + String.format( + """ + { + "titre": "Conférence API Test - Modifiée", + "description": "Description mise à jour", + "dateDebut": "%s", + "dateFin": "%s", + "lieu": "Nouveau lieu", + "typeEvenement": "CONFERENCE", + "capaciteMax": 75, + "prix": 25.00, + "inscriptionRequise": true, + "visiblePublic": true + } + """, + LocalDateTime.now().plusDays(16).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), + LocalDateTime.now() + .plusDays(16) + .plusHours(3) + .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); + + given() + .pathParam("id", evenementTestId) + .contentType(ContentType.JSON) + .body(evenementModifie) + .when() + .put("/api/evenements/{id}") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("titre", equalTo("Conférence API Test - Modifiée")) + .body("description", equalTo("Description mise à jour")) + .body("lieu", equalTo("Nouveau lieu")) + .body("capaciteMax", equalTo(75)) + .body("prix", equalTo(25.0f)); + } + + @Test + @Order(8) + @DisplayName("GET /api/evenements/a-venir - Événements à venir") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"MEMBRE"}) + void testEvenementsAVenir() { + given() + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .get("/api/evenements/a-venir") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @Order(9) + @DisplayName("GET /api/evenements/publics - Événements publics (non authentifié)") + void testEvenementsPublics_NonAuthentifie() { + given() + .queryParam("page", 0) + .queryParam("size", 20) + .when() + .get("/api/evenements/publics") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @Order(10) + @DisplayName("GET /api/evenements/recherche - Recherche d'événements") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"MEMBRE"}) + void testRechercherEvenements() { + given() + .queryParam("q", "Conférence") + .queryParam("page", 0) + .queryParam("size", 20) + .when() + .get("/api/evenements/recherche") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @Order(11) + @DisplayName("GET /api/evenements/recherche - Terme de recherche manquant") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"MEMBRE"}) + void testRechercherEvenements_TermeManquant() { + given() + .queryParam("page", 0) + .queryParam("size", 20) + .when() + .get("/api/evenements/recherche") + .then() + .statusCode(400) + .body("error", equalTo("Le terme de recherche est obligatoire")); + } + + @Test + @Order(12) + @DisplayName("GET /api/evenements/type/{type} - Événements par type") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"MEMBRE"}) + void testEvenementsParType() { + given() + .pathParam("type", "CONFERENCE") + .queryParam("page", 0) + .queryParam("size", 20) + .when() + .get("/api/evenements/type/{type}") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @Order(13) + @DisplayName("PATCH /api/evenements/{id}/statut - Changer statut") + @TestSecurity( + user = "admin@unionflow.dev", + roles = {"ADMIN"}) + void testChangerStatut() { + given() + .pathParam("id", evenementTestId) + .queryParam("statut", "CONFIRME") + .when() + .patch("/api/evenements/{id}/statut") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("statut", equalTo("CONFIRME")); + } + + @Test + @Order(14) + @DisplayName("GET /api/evenements/statistiques - Statistiques") + @TestSecurity( + user = "admin@unionflow.dev", + roles = {"ADMIN"}) + void testObtenirStatistiques() { + given() + .when() + .get("/api/evenements/statistiques") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("total", notNullValue()) + .body("actifs", notNullValue()) + .body("timestamp", notNullValue()); + } + + @Test + @Order(15) + @DisplayName("DELETE /api/evenements/{id} - Supprimer événement") + @TestSecurity( + user = "admin@unionflow.dev", + roles = {"ADMIN"}) + void testSupprimerEvenement() { + given() + .pathParam("id", evenementTestId) + .when() + .delete("/api/evenements/{id}") + .then() + .statusCode(204); + } + + @Test + @Order(16) + @DisplayName("Pagination - Paramètres valides") + @TestSecurity( + user = "marie.martin@test.com", + roles = {"MEMBRE"}) + void testPagination() { + given() + .queryParam("page", 0) + .queryParam("size", 5) + .queryParam("sort", "titre") + .queryParam("direction", "asc") + .when() + .get("/api/evenements") + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/resource/HealthResourceTest.java b/src/test.bak/java/dev/lions/unionflow/server/resource/HealthResourceTest.java new file mode 100644 index 0000000..fbe56d6 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/resource/HealthResourceTest.java @@ -0,0 +1,69 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Tests pour HealthResource + * + * @author Lions Dev Team + * @since 2025-01-10 + */ +@QuarkusTest +@DisplayName("Tests HealthResource") +class HealthResourceTest { + + @Test + @DisplayName("Test GET /api/status - Statut du serveur") + void testGetStatus() { + given() + .when() + .get("/api/status") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("status", equalTo("UP")) + .body("service", equalTo("UnionFlow Server")) + .body("version", equalTo("1.0.0")) + .body("message", equalTo("Serveur opérationnel")) + .body("timestamp", notNullValue()); + } + + @Test + @DisplayName("Test GET /api/status - Vérification de la structure de la réponse") + void testGetStatusStructure() { + given() + .when() + .get("/api/status") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("$", hasKey("status")) + .body("$", hasKey("service")) + .body("$", hasKey("version")) + .body("$", hasKey("timestamp")) + .body("$", hasKey("message")); + } + + @Test + @DisplayName("Test GET /api/status - Vérification du Content-Type") + void testGetStatusContentType() { + given().when().get("/api/status").then().statusCode(200).contentType("application/json"); + } + + @Test + @DisplayName("Test GET /api/status - Réponse rapide") + void testGetStatusPerformance() { + given() + .when() + .get("/api/status") + .then() + .statusCode(200) + .time(lessThan(1000L)); // Moins d'1 seconde + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/resource/MembreResourceCompleteIntegrationTest.java b/src/test.bak/java/dev/lions/unionflow/server/resource/MembreResourceCompleteIntegrationTest.java new file mode 100644 index 0000000..ea643b5 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/resource/MembreResourceCompleteIntegrationTest.java @@ -0,0 +1,318 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Tests d'intégration complets pour MembreResource Couvre tous les endpoints et cas d'erreur */ +@QuarkusTest +@DisplayName("Tests d'intégration complets MembreResource") +class MembreResourceCompleteIntegrationTest { + + @Test + @DisplayName("POST /api/membres - Création avec email existant") + void testCreerMembreEmailExistant() { + // Créer un premier membre + String membreJson1 = + """ + { + "numeroMembre": "UF2025-EXIST01", + "prenom": "Premier", + "nom": "Membre", + "email": "existe@test.com", + "telephone": "221701234567", + "dateNaissance": "1990-05-15", + "dateAdhesion": "2025-01-10", + "actif": true + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreJson1) + .when() + .post("/api/membres") + .then() + .statusCode(anyOf(is(201), is(400))); // 201 si nouveau, 400 si existe déjà + + // Essayer de créer un deuxième membre avec le même email + String membreJson2 = + """ + { + "numeroMembre": "UF2025-EXIST02", + "prenom": "Deuxieme", + "nom": "Membre", + "email": "existe@test.com", + "telephone": "221701234568", + "dateNaissance": "1985-08-20", + "dateAdhesion": "2025-01-10", + "actif": true + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreJson2) + .when() + .post("/api/membres") + .then() + .statusCode(400) + .body("message", notNullValue()); + } + + @Test + @DisplayName("POST /api/membres - Validation des champs obligatoires") + void testCreerMembreValidationChamps() { + // Test avec prénom manquant + String membreSansPrenom = + """ + { + "nom": "Test", + "email": "test.sans.prenom@test.com", + "telephone": "221701234567" + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreSansPrenom) + .when() + .post("/api/membres") + .then() + .statusCode(400); + + // Test avec email invalide + String membreEmailInvalide = + """ + { + "prenom": "Test", + "nom": "Test", + "email": "email-invalide", + "telephone": "221701234567" + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreEmailInvalide) + .when() + .post("/api/membres") + .then() + .statusCode(400); + } + + @Test + @DisplayName("PUT /api/membres/{id} - Mise à jour membre existant") + void testMettreAJourMembreExistant() { + // D'abord créer un membre + String membreOriginal = + """ + { + "numeroMembre": "UF2025-UPDATE01", + "prenom": "Original", + "nom": "Membre", + "email": "original.update@test.com", + "telephone": "221701234567", + "dateNaissance": "1990-05-15", + "dateAdhesion": "2025-01-10", + "actif": true + } + """; + + // Créer le membre (peut réussir ou échouer si existe déjà) + given() + .contentType(ContentType.JSON) + .body(membreOriginal) + .when() + .post("/api/membres") + .then() + .statusCode(anyOf(is(201), is(400))); + + // Essayer de mettre à jour avec ID 1 (peut exister ou non) + String membreMisAJour = + """ + { + "numeroMembre": "UF2025-UPDATE01", + "prenom": "Modifie", + "nom": "Membre", + "email": "modifie.update@test.com", + "telephone": "221701234567", + "dateNaissance": "1990-05-15", + "dateAdhesion": "2025-01-10", + "actif": true + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreMisAJour) + .when() + .put("/api/membres/1") + .then() + .statusCode(anyOf(is(200), is(400))); // 200 si trouvé, 400 si non trouvé + } + + @Test + @DisplayName("PUT /api/membres/{id} - Membre inexistant") + void testMettreAJourMembreInexistant() { + String membreJson = + """ + { + "numeroMembre": "UF2025-INEXIST01", + "prenom": "Inexistant", + "nom": "Membre", + "email": "inexistant@test.com", + "telephone": "221701234567", + "dateNaissance": "1990-05-15", + "dateAdhesion": "2025-01-10", + "actif": true + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreJson) + .when() + .put("/api/membres/99999") + .then() + .statusCode(400) + .body("message", notNullValue()); + } + + @Test + @DisplayName("DELETE /api/membres/{id} - Désactiver membre existant") + void testDesactiverMembreExistant() { + // Essayer de désactiver le membre ID 1 (peut exister ou non) + given() + .when() + .delete("/api/membres/1") + .then() + .statusCode(anyOf(is(204), is(404))); // 204 si trouvé, 404 si non trouvé + } + + @Test + @DisplayName("DELETE /api/membres/{id} - Membre inexistant") + void testDesactiverMembreInexistant() { + given() + .when() + .delete("/api/membres/99999") + .then() + .statusCode(404) + .body("message", notNullValue()); + } + + @Test + @DisplayName("GET /api/membres/{id} - Membre existant") + void testObtenirMembreExistant() { + // Essayer d'obtenir le membre ID 1 (peut exister ou non) + given() + .when() + .get("/api/membres/1") + .then() + .statusCode(anyOf(is(200), is(404))); // 200 si trouvé, 404 si non trouvé + } + + @Test + @DisplayName("GET /api/membres/{id} - Membre inexistant") + void testObtenirMembreInexistant() { + given() + .when() + .get("/api/membres/99999") + .then() + .statusCode(404) + .body("message", equalTo("Membre non trouvé")); + } + + @Test + @DisplayName("GET /api/membres/recherche - Recherche avec terme null") + void testRechercherMembresTermeNull() { + given() + .when() + .get("/api/membres/recherche") + .then() + .statusCode(400) + .body("message", equalTo("Le terme de recherche est requis")); + } + + @Test + @DisplayName("GET /api/membres/recherche - Recherche avec terme valide") + void testRechercherMembresTermeValide() { + given() + .queryParam("q", "test") + .when() + .get("/api/membres/recherche") + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + @Test + @DisplayName("Test des headers HTTP") + void testHeadersHTTP() { + // Test avec différents Accept headers + given() + .accept(ContentType.JSON) + .when() + .get("/api/membres") + .then() + .statusCode(200) + .contentType(ContentType.JSON); + + given() + .accept(ContentType.XML) + .when() + .get("/api/membres") + .then() + .statusCode(anyOf(is(200), is(406))); // 200 si supporté, 406 si non supporté + } + + @Test + @DisplayName("Test des méthodes HTTP non supportées") + void testMethodesHTTPNonSupportees() { + // OPTIONS peut être supporté ou non + given().when().options("/api/membres").then().statusCode(anyOf(is(200), is(405))); + + // HEAD peut être supporté ou non + given().when().head("/api/membres").then().statusCode(anyOf(is(200), is(405))); + } + + @Test + @DisplayName("Test de performance et robustesse") + void testPerformanceEtRobustesse() { + // Test avec une grande quantité de données + StringBuilder largeJson = new StringBuilder(); + largeJson.append("{"); + largeJson.append("\"prenom\": \"").append("A".repeat(100)).append("\","); + largeJson.append("\"nom\": \"").append("B".repeat(100)).append("\","); + largeJson.append("\"email\": \"large.test@test.com\","); + largeJson.append("\"telephone\": \"221701234567\""); + largeJson.append("}"); + + given() + .contentType(ContentType.JSON) + .body(largeJson.toString()) + .when() + .post("/api/membres") + .then() + .statusCode(anyOf(is(201), is(400))); // Peut réussir ou échouer selon la validation + } + + @Test + @DisplayName("Test de gestion des erreurs serveur") + void testGestionErreursServeur() { + // Test avec des données qui peuvent causer des erreurs internes + String jsonMalformed = "{ invalid json }"; + + given() + .contentType(ContentType.JSON) + .body(jsonMalformed) + .when() + .post("/api/membres") + .then() + .statusCode(400); // Bad Request pour JSON malformé + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/resource/MembreResourceSimpleIntegrationTest.java b/src/test.bak/java/dev/lions/unionflow/server/resource/MembreResourceSimpleIntegrationTest.java new file mode 100644 index 0000000..e4aa5b3 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/resource/MembreResourceSimpleIntegrationTest.java @@ -0,0 +1,259 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Tests d'intégration simples pour MembreResource + * + * @author Lions Dev Team + * @since 2025-01-10 + */ +@QuarkusTest +@DisplayName("Tests d'intégration simples MembreResource") +class MembreResourceSimpleIntegrationTest { + + @Test + @DisplayName("GET /api/membres - Lister tous les membres actifs") + void testListerMembres() { + given() + .when() + .get("/api/membres") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("$", notNullValue()); + } + + @Test + @DisplayName("GET /api/membres/999 - Membre non trouvé") + void testObtenirMembreNonTrouve() { + given() + .when() + .get("/api/membres/999") + .then() + .statusCode(404) + .contentType(ContentType.JSON) + .body("message", equalTo("Membre non trouvé")); + } + + @Test + @DisplayName("POST /api/membres - Données invalides") + void testCreerMembreDonneesInvalides() { + String membreJson = + """ + { + "prenom": "", + "nom": "", + "email": "email-invalide", + "telephone": "123", + "dateNaissance": "2030-01-01" + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreJson) + .when() + .post("/api/membres") + .then() + .statusCode(400); + } + + @Test + @DisplayName("PUT /api/membres/999 - Membre non trouvé") + void testMettreAJourMembreNonTrouve() { + String membreJson = + """ + { + "prenom": "Pierre", + "nom": "Martin", + "email": "pierre.martin@test.com" + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreJson) + .when() + .put("/api/membres/999") + .then() + .statusCode(400); // Simplement vérifier le code de statut + } + + @Test + @DisplayName("DELETE /api/membres/999 - Membre non trouvé") + void testDesactiverMembreNonTrouve() { + given() + .when() + .delete("/api/membres/999") + .then() + .statusCode(404) + .contentType(ContentType.JSON) + .body("message", containsString("Membre non trouvé")); + } + + @Test + @DisplayName("GET /api/membres/recherche - Terme manquant") + void testRechercherMembresTermeManquant() { + given() + .when() + .get("/api/membres/recherche") + .then() + .statusCode(400) + .contentType(ContentType.JSON) + .body("message", equalTo("Le terme de recherche est requis")); + } + + @Test + @DisplayName("GET /api/membres/recherche - Terme vide") + void testRechercherMembresTermeVide() { + given() + .queryParam("q", " ") + .when() + .get("/api/membres/recherche") + .then() + .statusCode(400) + .contentType(ContentType.JSON) + .body("message", equalTo("Le terme de recherche est requis")); + } + + @Test + @DisplayName("GET /api/membres/recherche - Recherche valide") + void testRechercherMembresValide() { + given() + .queryParam("q", "test") + .when() + .get("/api/membres/recherche") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("$", notNullValue()); + } + + @Test + @DisplayName("GET /api/membres/stats - Statistiques") + void testObtenirStatistiques() { + given() + .when() + .get("/api/membres/stats") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("nombreMembresActifs", notNullValue()) + .body("timestamp", notNullValue()); + } + + @Test + @DisplayName("POST /api/membres - Membre valide") + void testCreerMembreValide() { + String membreJson = + """ + { + "prenom": "Jean", + "nom": "Dupont", + "email": "jean.dupont.test@example.com", + "telephone": "221701234567", + "dateNaissance": "1990-05-15", + "dateAdhesion": "2025-01-10" + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreJson) + .when() + .post("/api/membres") + .then() + .statusCode(anyOf(is(201), is(400))); // 201 si succès, 400 si email existe déjà + } + + @Test + @DisplayName("Test des endpoints avec différents content types") + void testContentTypes() { + // Test avec Accept header + given() + .accept(ContentType.JSON) + .when() + .get("/api/membres") + .then() + .statusCode(200) + .contentType(ContentType.JSON); + + // Test avec Accept header pour les stats + given() + .accept(ContentType.JSON) + .when() + .get("/api/membres/stats") + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + @Test + @DisplayName("Test des méthodes HTTP non supportées") + void testMethodesNonSupportees() { + // PATCH n'est pas supporté + given().when().patch("/api/membres/1").then().statusCode(405); // Method Not Allowed + } + + @Test + @DisplayName("PUT /api/membres/{id} - Mise à jour avec données invalides") + void testMettreAJourMembreAvecDonneesInvalides() { + String membreInvalideJson = + """ + { + "prenom": "", + "nom": "", + "email": "email-invalide" + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreInvalideJson) + .when() + .put("/api/membres/1") + .then() + .statusCode(400); + } + + @Test + @DisplayName("POST /api/membres - Données invalides") + void testCreerMembreAvecDonneesInvalides() { + String membreInvalideJson = + """ + { + "prenom": "", + "nom": "", + "email": "email-invalide" + } + """; + + given() + .contentType(ContentType.JSON) + .body(membreInvalideJson) + .when() + .post("/api/membres") + .then() + .statusCode(400); + } + + @Test + @DisplayName("GET /api/membres/recherche - Terme avec espaces seulement") + void testRechercherMembresTermeAvecEspacesUniquement() { + given() + .queryParam("q", " ") + .when() + .get("/api/membres/recherche") + .then() + .statusCode(400) + .contentType(ContentType.JSON) + .body("message", equalTo("Le terme de recherche est requis")); + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/resource/MembreResourceTest.java b/src/test.bak/java/dev/lions/unionflow/server/resource/MembreResourceTest.java new file mode 100644 index 0000000..5f658e7 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/resource/MembreResourceTest.java @@ -0,0 +1,275 @@ +package dev.lions.unionflow.server.resource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +import dev.lions.unionflow.server.api.dto.membre.MembreDTO; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.service.MembreService; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import jakarta.ws.rs.core.Response; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests pour MembreResource + * + * @author Lions Dev Team + * @since 2025-01-10 + */ +@ExtendWith(MockitoExtension.class) +@DisplayName("Tests MembreResource") +class MembreResourceTest { + + @InjectMocks MembreResource membreResource; + + @Mock MembreService membreService; + + @Test + @DisplayName("Test de l'existence de la classe MembreResource") + void testMembreResourceExists() { + // Given & When & Then + assertThat(MembreResource.class).isNotNull(); + assertThat(membreResource).isNotNull(); + } + + @Test + @DisplayName("Test de l'annotation Path") + void testPathAnnotation() { + // Given & When & Then + assertThat(MembreResource.class.getAnnotation(jakarta.ws.rs.Path.class)).isNotNull(); + assertThat(MembreResource.class.getAnnotation(jakarta.ws.rs.Path.class).value()) + .isEqualTo("/api/membres"); + } + + @Test + @DisplayName("Test de l'annotation ApplicationScoped") + void testApplicationScopedAnnotation() { + // Given & When & Then + assertThat( + MembreResource.class.getAnnotation(jakarta.enterprise.context.ApplicationScoped.class)) + .isNotNull(); + } + + @Test + @DisplayName("Test de l'annotation Produces") + void testProducesAnnotation() { + // Given & When & Then + assertThat(MembreResource.class.getAnnotation(jakarta.ws.rs.Produces.class)).isNotNull(); + assertThat(MembreResource.class.getAnnotation(jakarta.ws.rs.Produces.class).value()) + .contains("application/json"); + } + + @Test + @DisplayName("Test de l'annotation Consumes") + void testConsumesAnnotation() { + // Given & When & Then + assertThat(MembreResource.class.getAnnotation(jakarta.ws.rs.Consumes.class)).isNotNull(); + assertThat(MembreResource.class.getAnnotation(jakarta.ws.rs.Consumes.class).value()) + .contains("application/json"); + } + + @Test + @DisplayName("Test des méthodes du resource") + void testResourceMethods() throws NoSuchMethodException { + // Given & When & Then + assertThat(MembreResource.class.getMethod("listerMembres")).isNotNull(); + assertThat(MembreResource.class.getMethod("obtenirMembre", Long.class)).isNotNull(); + assertThat(MembreResource.class.getMethod("creerMembre", Membre.class)).isNotNull(); + assertThat(MembreResource.class.getMethod("mettreAJourMembre", Long.class, Membre.class)) + .isNotNull(); + assertThat(MembreResource.class.getMethod("desactiverMembre", Long.class)).isNotNull(); + assertThat(MembreResource.class.getMethod("rechercherMembres", String.class)).isNotNull(); + assertThat(MembreResource.class.getMethod("obtenirStatistiques")).isNotNull(); + } + + @Test + @DisplayName("Test de la création d'instance") + void testInstanceCreation() { + // Given & When + MembreResource resource = new MembreResource(); + + // Then + assertThat(resource).isNotNull(); + } + + @Test + @DisplayName("Test listerMembres") + void testListerMembres() { + // Given + List membres = + Arrays.asList(createTestMembre("Jean", "Dupont"), createTestMembre("Marie", "Martin")); + List membresDTO = + Arrays.asList( + createTestMembreDTO("Jean", "Dupont"), createTestMembreDTO("Marie", "Martin")); + + when(membreService.listerMembresActifs(any(Page.class), any(Sort.class))).thenReturn(membres); + when(membreService.convertToDTOList(membres)).thenReturn(membresDTO); + + // When + Response response = membreResource.listerMembres(0, 20, "nom", "asc"); + + // Then + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getEntity()).isEqualTo(membresDTO); + } + + @Test + @DisplayName("Test obtenirMembre") + void testObtenirMembre() { + // Given + Long id = 1L; + Membre membre = createTestMembre("Jean", "Dupont"); + when(membreService.trouverParId(id)).thenReturn(Optional.of(membre)); + + // When + Response response = membreResource.obtenirMembre(id); + + // Then + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getEntity()).isEqualTo(membre); + } + + @Test + @DisplayName("Test obtenirMembre - membre non trouvé") + void testObtenirMembreNonTrouve() { + // Given + Long id = 999L; + when(membreService.trouverParId(id)).thenReturn(Optional.empty()); + + // When + Response response = membreResource.obtenirMembre(id); + + // Then + assertThat(response.getStatus()).isEqualTo(404); + } + + @Test + @DisplayName("Test creerMembre") + void testCreerMembre() { + // Given + MembreDTO membreDTO = createTestMembreDTO("Jean", "Dupont"); + Membre membre = createTestMembre("Jean", "Dupont"); + Membre membreCreated = createTestMembre("Jean", "Dupont"); + membreCreated.id = 1L; + MembreDTO membreCreatedDTO = createTestMembreDTO("Jean", "Dupont"); + + when(membreService.convertFromDTO(any(MembreDTO.class))).thenReturn(membre); + when(membreService.creerMembre(any(Membre.class))).thenReturn(membreCreated); + when(membreService.convertToDTO(any(Membre.class))).thenReturn(membreCreatedDTO); + + // When + Response response = membreResource.creerMembre(membreDTO); + + // Then + assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getEntity()).isEqualTo(membreCreatedDTO); + } + + @Test + @DisplayName("Test mettreAJourMembre") + void testMettreAJourMembre() { + // Given + Long id = 1L; + MembreDTO membreDTO = createTestMembreDTO("Jean", "Dupont"); + Membre membre = createTestMembre("Jean", "Dupont"); + Membre membreUpdated = createTestMembre("Jean", "Martin"); + membreUpdated.id = id; + MembreDTO membreUpdatedDTO = createTestMembreDTO("Jean", "Martin"); + + when(membreService.convertFromDTO(any(MembreDTO.class))).thenReturn(membre); + when(membreService.mettreAJourMembre(anyLong(), any(Membre.class))).thenReturn(membreUpdated); + when(membreService.convertToDTO(any(Membre.class))).thenReturn(membreUpdatedDTO); + + // When + Response response = membreResource.mettreAJourMembre(id, membreDTO); + + // Then + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getEntity()).isEqualTo(membreUpdatedDTO); + } + + @Test + @DisplayName("Test desactiverMembre") + void testDesactiverMembre() { + // Given + Long id = 1L; + + // When + Response response = membreResource.desactiverMembre(id); + + // Then + assertThat(response.getStatus()).isEqualTo(204); + } + + @Test + @DisplayName("Test rechercherMembres") + void testRechercherMembres() { + // Given + String recherche = "Jean"; + List membres = Arrays.asList(createTestMembre("Jean", "Dupont")); + List membresDTO = Arrays.asList(createTestMembreDTO("Jean", "Dupont")); + when(membreService.rechercherMembres(anyString(), any(Page.class), any(Sort.class))) + .thenReturn(membres); + when(membreService.convertToDTOList(membres)).thenReturn(membresDTO); + + // When + Response response = membreResource.rechercherMembres(recherche, 0, 20, "nom", "asc"); + + // Then + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getEntity()).isEqualTo(membresDTO); + } + + @Test + @DisplayName("Test obtenirStatistiques") + void testObtenirStatistiques() { + // Given + long count = 42L; + when(membreService.compterMembresActifs()).thenReturn(count); + + // When + Response response = membreResource.obtenirStatistiques(); + + // Then + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getEntity()).isInstanceOf(java.util.Map.class); + } + + private Membre createTestMembre(String prenom, String nom) { + Membre membre = new Membre(); + membre.setPrenom(prenom); + membre.setNom(nom); + membre.setEmail(prenom.toLowerCase() + "." + nom.toLowerCase() + "@test.com"); + membre.setTelephone("221701234567"); + membre.setDateNaissance(LocalDate.of(1990, 1, 1)); + membre.setDateAdhesion(LocalDate.now()); + membre.setActif(true); + membre.setNumeroMembre("UF-2025-TEST01"); + return membre; + } + + private MembreDTO createTestMembreDTO(String prenom, String nom) { + MembreDTO dto = new MembreDTO(); + dto.setPrenom(prenom); + dto.setNom(nom); + dto.setEmail(prenom.toLowerCase() + "." + nom.toLowerCase() + "@test.com"); + dto.setTelephone("221701234567"); + dto.setDateNaissance(LocalDate.of(1990, 1, 1)); + dto.setDateAdhesion(LocalDate.now()); + dto.setStatut("ACTIF"); + dto.setNumeroMembre("UF-2025-TEST01"); + dto.setAssociationId(1L); + return dto; + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/resource/OrganisationResourceTest.java b/src/test.bak/java/dev/lions/unionflow/server/resource/OrganisationResourceTest.java new file mode 100644 index 0000000..6a313a3 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/resource/OrganisationResourceTest.java @@ -0,0 +1,345 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; + +import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import java.time.LocalDateTime; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +/** + * Tests d'intégration pour OrganisationResource + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@QuarkusTest +class OrganisationResourceTest { + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testCreerOrganisation_Success() { + OrganisationDTO organisation = createTestOrganisationDTO(); + + given() + .contentType(ContentType.JSON) + .body(organisation) + .when() + .post("/api/organisations") + .then() + .statusCode(201) + .body("nom", equalTo("Lions Club Test API")) + .body("email", equalTo("testapi@lionsclub.org")) + .body("actif", equalTo(true)); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testCreerOrganisation_EmailInvalide() { + OrganisationDTO organisation = createTestOrganisationDTO(); + organisation.setEmail("email-invalide"); + + given() + .contentType(ContentType.JSON) + .body(organisation) + .when() + .post("/api/organisations") + .then() + .statusCode(400); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testCreerOrganisation_NomVide() { + OrganisationDTO organisation = createTestOrganisationDTO(); + organisation.setNom(""); + + given() + .contentType(ContentType.JSON) + .body(organisation) + .when() + .post("/api/organisations") + .then() + .statusCode(400); + } + + @Test + void testCreerOrganisation_NonAuthentifie() { + OrganisationDTO organisation = createTestOrganisationDTO(); + + given() + .contentType(ContentType.JSON) + .body(organisation) + .when() + .post("/api/organisations") + .then() + .statusCode(401); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testListerOrganisations_Success() { + given() + .when() + .get("/api/organisations") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testListerOrganisations_AvecPagination() { + given() + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .get("/api/organisations") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testListerOrganisations_AvecRecherche() { + given() + .queryParam("recherche", "Lions") + .when() + .get("/api/organisations") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + void testListerOrganisations_NonAuthentifie() { + given().when().get("/api/organisations").then().statusCode(401); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testObtenirOrganisation_NonTrouvee() { + given() + .when() + .get("/api/organisations/99999") + .then() + .statusCode(404) + .body("error", equalTo("Organisation non trouvée")); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testMettreAJourOrganisation_NonTrouvee() { + OrganisationDTO organisation = createTestOrganisationDTO(); + + given() + .contentType(ContentType.JSON) + .body(organisation) + .when() + .put("/api/organisations/99999") + .then() + .statusCode(404) + .body("error", containsString("Organisation non trouvée")); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testSupprimerOrganisation_NonTrouvee() { + given() + .when() + .delete("/api/organisations/99999") + .then() + .statusCode(404) + .body("error", containsString("Organisation non trouvée")); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testRechercheAvancee_Success() { + given() + .queryParam("nom", "Lions") + .queryParam("ville", "Abidjan") + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .get("/api/organisations/recherche") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testRechercheAvancee_SansCriteres() { + given() + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .get("/api/organisations/recherche") + .then() + .statusCode(200) + .body("size()", greaterThanOrEqualTo(0)); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testActiverOrganisation_NonTrouvee() { + given() + .when() + .post("/api/organisations/99999/activer") + .then() + .statusCode(404) + .body("error", containsString("Organisation non trouvée")); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testSuspendreOrganisation_NonTrouvee() { + given() + .when() + .post("/api/organisations/99999/suspendre") + .then() + .statusCode(404) + .body("error", containsString("Organisation non trouvée")); + } + + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testObtenirStatistiques_Success() { + given() + .when() + .get("/api/organisations/statistiques") + .then() + .statusCode(200) + .body("totalOrganisations", notNullValue()) + .body("organisationsActives", notNullValue()) + .body("organisationsInactives", notNullValue()) + .body("nouvellesOrganisations30Jours", notNullValue()) + .body("tauxActivite", notNullValue()) + .body("timestamp", notNullValue()); + } + + @Test + void testObtenirStatistiques_NonAuthentifie() { + given().when().get("/api/organisations/statistiques").then().statusCode(401); + } + + /** Test de workflow complet : création, lecture, mise à jour, suppression */ + @Test + @TestSecurity( + user = "testUser", + roles = {"ADMIN"}) + void testWorkflowComplet() { + // 1. Créer une organisation + OrganisationDTO organisation = createTestOrganisationDTO(); + organisation.setNom("Lions Club Workflow Test"); + organisation.setEmail("workflow@lionsclub.org"); + + String location = + given() + .contentType(ContentType.JSON) + .body(organisation) + .when() + .post("/api/organisations") + .then() + .statusCode(201) + .extract() + .header("Location"); + + // Extraire l'ID de l'organisation créée + String organisationId = location.substring(location.lastIndexOf("/") + 1); + + // 2. Lire l'organisation créée + given() + .when() + .get("/api/organisations/" + organisationId) + .then() + .statusCode(200) + .body("nom", equalTo("Lions Club Workflow Test")) + .body("email", equalTo("workflow@lionsclub.org")); + + // 3. Mettre à jour l'organisation + organisation.setDescription("Description mise à jour"); + given() + .contentType(ContentType.JSON) + .body(organisation) + .when() + .put("/api/organisations/" + organisationId) + .then() + .statusCode(200) + .body("description", equalTo("Description mise à jour")); + + // 4. Suspendre l'organisation + given() + .when() + .post("/api/organisations/" + organisationId + "/suspendre") + .then() + .statusCode(200); + + // 5. Activer l'organisation + given().when().post("/api/organisations/" + organisationId + "/activer").then().statusCode(200); + + // 6. Supprimer l'organisation (soft delete) + given().when().delete("/api/organisations/" + organisationId).then().statusCode(204); + } + + /** Crée un DTO d'organisation pour les tests */ + private OrganisationDTO createTestOrganisationDTO() { + OrganisationDTO dto = new OrganisationDTO(); + dto.setId(UUID.randomUUID()); + dto.setNom("Lions Club Test API"); + dto.setNomCourt("LC Test API"); + dto.setEmail("testapi@lionsclub.org"); + dto.setDescription("Organisation de test pour l'API"); + dto.setTelephone("+225 01 02 03 04 05"); + dto.setAdresse("123 Rue de Test API"); + dto.setVille("Abidjan"); + dto.setCodePostal("00225"); + dto.setRegion("Lagunes"); + dto.setPays("Côte d'Ivoire"); + dto.setSiteWeb("https://testapi.lionsclub.org"); + dto.setObjectifs("Servir la communauté"); + dto.setActivitesPrincipales("Actions sociales et humanitaires"); + dto.setNombreMembres(0); + dto.setDateCreation(LocalDateTime.now()); + dto.setActif(true); + dto.setVersion(0L); + + return dto; + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/service/AideServiceTest.java b/src/test.bak/java/dev/lions/unionflow/server/service/AideServiceTest.java new file mode 100644 index 0000000..e82ad49 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/service/AideServiceTest.java @@ -0,0 +1,327 @@ +package dev.lions.unionflow.server.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import dev.lions.unionflow.server.api.dto.solidarite.aide.AideDTO; +import dev.lions.unionflow.server.api.enums.solidarite.StatutAide; +import dev.lions.unionflow.server.api.enums.solidarite.TypeAide; +import dev.lions.unionflow.server.entity.Aide; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.AideRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import dev.lions.unionflow.server.security.KeycloakService; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; + +/** + * Tests unitaires pour AideService + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@QuarkusTest +@DisplayName("AideService - Tests unitaires") +class AideServiceTest { + + @Inject AideService aideService; + + @Mock AideRepository aideRepository; + + @Mock MembreRepository membreRepository; + + @Mock OrganisationRepository organisationRepository; + + @Mock KeycloakService keycloakService; + + private Membre membreTest; + private Organisation organisationTest; + private Aide aideTest; + private AideDTO aideDTOTest; + + @BeforeEach + void setUp() { + // Membre de test + membreTest = new Membre(); + membreTest.id = 1L; + membreTest.setNumeroMembre("UF-2025-TEST001"); + membreTest.setNom("Dupont"); + membreTest.setPrenom("Jean"); + membreTest.setEmail("jean.dupont@test.com"); + membreTest.setActif(true); + + // Organisation de test + organisationTest = new Organisation(); + organisationTest.id = 1L; + organisationTest.setNom("Lions Club Test"); + organisationTest.setEmail("contact@lionstest.com"); + organisationTest.setActif(true); + + // Aide de test + aideTest = new Aide(); + aideTest.id = 1L; + aideTest.setNumeroReference("AIDE-2025-TEST01"); + aideTest.setTitre("Aide médicale urgente"); + aideTest.setDescription("Demande d'aide pour frais médicaux urgents"); + aideTest.setTypeAide(TypeAide.AIDE_FRAIS_MEDICAUX); + aideTest.setMontantDemande(new BigDecimal("500000.00")); + aideTest.setStatut(StatutAide.EN_ATTENTE); + aideTest.setPriorite("URGENTE"); + aideTest.setMembreDemandeur(membreTest); + aideTest.setOrganisation(organisationTest); + aideTest.setActif(true); + aideTest.setDateCreation(LocalDateTime.now()); + + // DTO de test + aideDTOTest = new AideDTO(); + aideDTOTest.setId(UUID.randomUUID()); + aideDTOTest.setNumeroReference("AIDE-2025-TEST01"); + aideDTOTest.setTitre("Aide médicale urgente"); + aideDTOTest.setDescription("Demande d'aide pour frais médicaux urgents"); + aideDTOTest.setTypeAide("MEDICALE"); + aideDTOTest.setMontantDemande(new BigDecimal("500000.00")); + aideDTOTest.setStatut("EN_ATTENTE"); + aideDTOTest.setPriorite("URGENTE"); + aideDTOTest.setMembreDemandeurId(UUID.randomUUID()); + aideDTOTest.setAssociationId(UUID.randomUUID()); + aideDTOTest.setActif(true); + } + + @Nested + @DisplayName("Tests de création d'aide") + class CreationAideTests { + + @Test + @DisplayName("Création d'aide réussie") + void testCreerAide_Success() { + // Given + when(membreRepository.findByIdOptional(anyLong())).thenReturn(Optional.of(membreTest)); + when(organisationRepository.findByIdOptional(anyLong())) + .thenReturn(Optional.of(organisationTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com"); + + ArgumentCaptor aideCaptor = ArgumentCaptor.forClass(Aide.class); + doNothing().when(aideRepository).persist(aideCaptor.capture()); + + // When + AideDTO result = aideService.creerAide(aideDTOTest); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTitre()).isEqualTo(aideDTOTest.getTitre()); + assertThat(result.getDescription()).isEqualTo(aideDTOTest.getDescription()); + + Aide aidePersistee = aideCaptor.getValue(); + assertThat(aidePersistee.getTitre()).isEqualTo(aideDTOTest.getTitre()); + assertThat(aidePersistee.getMembreDemandeur()).isEqualTo(membreTest); + assertThat(aidePersistee.getOrganisation()).isEqualTo(organisationTest); + assertThat(aidePersistee.getCreePar()).isEqualTo("admin@test.com"); + + verify(aideRepository).persist(any(Aide.class)); + } + + @Test + @DisplayName("Création d'aide - Membre non trouvé") + void testCreerAide_MembreNonTrouve() { + // Given + when(membreRepository.findByIdOptional(anyLong())).thenReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> aideService.creerAide(aideDTOTest)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Membre demandeur non trouvé"); + + verify(aideRepository, never()).persist(any(Aide.class)); + } + + @Test + @DisplayName("Création d'aide - Organisation non trouvée") + void testCreerAide_OrganisationNonTrouvee() { + // Given + when(membreRepository.findByIdOptional(anyLong())).thenReturn(Optional.of(membreTest)); + when(organisationRepository.findByIdOptional(anyLong())).thenReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> aideService.creerAide(aideDTOTest)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Organisation non trouvée"); + + verify(aideRepository, never()).persist(any(Aide.class)); + } + + @Test + @DisplayName("Création d'aide - Montant invalide") + void testCreerAide_MontantInvalide() { + // Given + aideDTOTest.setMontantDemande(new BigDecimal("-100.00")); + when(membreRepository.findByIdOptional(anyLong())).thenReturn(Optional.of(membreTest)); + when(organisationRepository.findByIdOptional(anyLong())) + .thenReturn(Optional.of(organisationTest)); + + // When & Then + assertThatThrownBy(() -> aideService.creerAide(aideDTOTest)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Le montant demandé doit être positif"); + + verify(aideRepository, never()).persist(any(Aide.class)); + } + } + + @Nested + @DisplayName("Tests de récupération d'aide") + class RecuperationAideTests { + + @Test + @DisplayName("Récupération d'aide par ID réussie") + void testObtenirAideParId_Success() { + // Given + when(aideRepository.findByIdOptional(1L)).thenReturn(Optional.of(aideTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("autre@test.com"); + + // When + AideDTO result = aideService.obtenirAideParId(1L); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTitre()).isEqualTo(aideTest.getTitre()); + assertThat(result.getDescription()).isEqualTo(aideTest.getDescription()); + assertThat(result.getStatut()).isEqualTo(aideTest.getStatut().name()); + } + + @Test + @DisplayName("Récupération d'aide par ID - Non trouvée") + void testObtenirAideParId_NonTrouvee() { + // Given + when(aideRepository.findByIdOptional(999L)).thenReturn(Optional.empty()); + + // When & Then + assertThatThrownBy(() -> aideService.obtenirAideParId(999L)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("Demande d'aide non trouvée"); + } + + @Test + @DisplayName("Récupération d'aide par référence réussie") + void testObtenirAideParReference_Success() { + // Given + String reference = "AIDE-2025-TEST01"; + when(aideRepository.findByNumeroReference(reference)).thenReturn(Optional.of(aideTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("autre@test.com"); + + // When + AideDTO result = aideService.obtenirAideParReference(reference); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getNumeroReference()).isEqualTo(reference); + } + } + + @Nested + @DisplayName("Tests de mise à jour d'aide") + class MiseAJourAideTests { + + @Test + @DisplayName("Mise à jour d'aide réussie") + void testMettreAJourAide_Success() { + // Given + when(aideRepository.findByIdOptional(1L)).thenReturn(Optional.of(aideTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("jean.dupont@test.com"); + when(keycloakService.hasRole("admin")).thenReturn(false); + when(keycloakService.hasRole("gestionnaire_aide")).thenReturn(false); + + AideDTO aideMiseAJour = new AideDTO(); + aideMiseAJour.setTitre("Titre modifié"); + aideMiseAJour.setDescription("Description modifiée"); + aideMiseAJour.setMontantDemande(new BigDecimal("600000.00")); + aideMiseAJour.setPriorite("HAUTE"); + + // When + AideDTO result = aideService.mettreAJourAide(1L, aideMiseAJour); + + // Then + assertThat(result).isNotNull(); + assertThat(aideTest.getTitre()).isEqualTo("Titre modifié"); + assertThat(aideTest.getDescription()).isEqualTo("Description modifiée"); + assertThat(aideTest.getMontantDemande()).isEqualTo(new BigDecimal("600000.00")); + assertThat(aideTest.getPriorite()).isEqualTo("HAUTE"); + } + + @Test + @DisplayName("Mise à jour d'aide - Accès non autorisé") + void testMettreAJourAide_AccesNonAutorise() { + // Given + when(aideRepository.findByIdOptional(1L)).thenReturn(Optional.of(aideTest)); + when(keycloakService.getCurrentUserEmail()).thenReturn("autre@test.com"); + when(keycloakService.hasRole("admin")).thenReturn(false); + when(keycloakService.hasRole("gestionnaire_aide")).thenReturn(false); + + AideDTO aideMiseAJour = new AideDTO(); + aideMiseAJour.setTitre("Titre modifié"); + + // When & Then + assertThatThrownBy(() -> aideService.mettreAJourAide(1L, aideMiseAJour)) + .isInstanceOf(SecurityException.class) + .hasMessageContaining("Vous n'avez pas les permissions"); + } + } + + @Nested + @DisplayName("Tests de conversion DTO/Entity") + class ConversionTests { + + @Test + @DisplayName("Conversion Entity vers DTO") + void testConvertToDTO() { + // When + AideDTO result = aideService.convertToDTO(aideTest); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTitre()).isEqualTo(aideTest.getTitre()); + assertThat(result.getDescription()).isEqualTo(aideTest.getDescription()); + assertThat(result.getMontantDemande()).isEqualTo(aideTest.getMontantDemande()); + assertThat(result.getStatut()).isEqualTo(aideTest.getStatut().name()); + assertThat(result.getTypeAide()).isEqualTo(aideTest.getTypeAide().name()); + } + + @Test + @DisplayName("Conversion DTO vers Entity") + void testConvertFromDTO() { + // When + Aide result = aideService.convertFromDTO(aideDTOTest); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTitre()).isEqualTo(aideDTOTest.getTitre()); + assertThat(result.getDescription()).isEqualTo(aideDTOTest.getDescription()); + assertThat(result.getMontantDemande()).isEqualTo(aideDTOTest.getMontantDemande()); + assertThat(result.getStatut()).isEqualTo(StatutAide.EN_ATTENTE); + assertThat(result.getTypeAide()).isEqualTo(TypeAide.AIDE_FRAIS_MEDICAUX); + } + + @Test + @DisplayName("Conversion DTO null") + void testConvertFromDTO_Null() { + // When + Aide result = aideService.convertFromDTO(null); + + // Then + assertThat(result).isNull(); + } + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/service/EvenementServiceTest.java b/src/test.bak/java/dev/lions/unionflow/server/service/EvenementServiceTest.java new file mode 100644 index 0000000..9d4dcf0 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/service/EvenementServiceTest.java @@ -0,0 +1,403 @@ +package dev.lions.unionflow.server.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import dev.lions.unionflow.server.entity.Evenement; +import dev.lions.unionflow.server.entity.Evenement.StatutEvenement; +import dev.lions.unionflow.server.entity.Evenement.TypeEvenement; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.EvenementRepository; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.*; +import org.mockito.Mock; + +/** + * Tests unitaires pour EvenementService + * + *

Tests complets du service de gestion des événements avec validation des règles métier et + * intégration Keycloak. + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@DisplayName("Tests unitaires - Service Événements") +class EvenementServiceTest { + + @Inject EvenementService evenementService; + + @Mock EvenementRepository evenementRepository; + + @Mock MembreRepository membreRepository; + + @Mock OrganisationRepository organisationRepository; + + @Mock KeycloakService keycloakService; + + private Evenement evenementTest; + private Organisation organisationTest; + private Membre membreTest; + + @BeforeEach + void setUp() { + // Données de test + organisationTest = + Organisation.builder() + .nom("Union Test") + .typeOrganisation("ASSOCIATION") + .statut("ACTIVE") + .email("test@union.com") + .actif(true) + .build(); + organisationTest.id = 1L; + + membreTest = + Membre.builder() + .numeroMembre("UF2025-TEST01") + .prenom("Jean") + .nom("Dupont") + .email("jean.dupont@test.com") + .actif(true) + .build(); + membreTest.id = 1L; + + evenementTest = + Evenement.builder() + .titre("Assemblée Générale 2025") + .description("Assemblée générale annuelle de l'union") + .dateDebut(LocalDateTime.now().plusDays(30)) + .dateFin(LocalDateTime.now().plusDays(30).plusHours(3)) + .lieu("Salle de conférence") + .typeEvenement(TypeEvenement.ASSEMBLEE_GENERALE) + .statut(StatutEvenement.PLANIFIE) + .capaciteMax(100) + .prix(BigDecimal.valueOf(25.00)) + .inscriptionRequise(true) + .visiblePublic(true) + .actif(true) + .organisation(organisationTest) + .organisateur(membreTest) + .build(); + evenementTest.id = 1L; + } + + @Test + @Order(1) + @DisplayName("Création d'événement - Succès") + void testCreerEvenement_Succes() { + // Given + when(keycloakService.getCurrentUserEmail()).thenReturn("jean.dupont@test.com"); + when(evenementRepository.findByTitre(anyString())).thenReturn(Optional.empty()); + doNothing().when(evenementRepository).persist(any(Evenement.class)); + + // When + Evenement resultat = evenementService.creerEvenement(evenementTest); + + // Then + assertNotNull(resultat); + assertEquals("Assemblée Générale 2025", resultat.getTitre()); + assertEquals(StatutEvenement.PLANIFIE, resultat.getStatut()); + assertTrue(resultat.getActif()); + assertEquals("jean.dupont@test.com", resultat.getCreePar()); + + verify(evenementRepository).persist(any(Evenement.class)); + } + + @Test + @Order(2) + @DisplayName("Création d'événement - Titre obligatoire") + void testCreerEvenement_TitreObligatoire() { + // Given + evenementTest.setTitre(null); + + // When & Then + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, () -> evenementService.creerEvenement(evenementTest)); + + assertEquals("Le titre de l'événement est obligatoire", exception.getMessage()); + verify(evenementRepository, never()).persist(any(Evenement.class)); + } + + @Test + @Order(3) + @DisplayName("Création d'événement - Date de début obligatoire") + void testCreerEvenement_DateDebutObligatoire() { + // Given + evenementTest.setDateDebut(null); + + // When & Then + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, () -> evenementService.creerEvenement(evenementTest)); + + assertEquals("La date de début est obligatoire", exception.getMessage()); + } + + @Test + @Order(4) + @DisplayName("Création d'événement - Date de début dans le passé") + void testCreerEvenement_DateDebutPassee() { + // Given + evenementTest.setDateDebut(LocalDateTime.now().minusDays(1)); + + // When & Then + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, () -> evenementService.creerEvenement(evenementTest)); + + assertEquals("La date de début ne peut pas être dans le passé", exception.getMessage()); + } + + @Test + @Order(5) + @DisplayName("Création d'événement - Date de fin antérieure à date de début") + void testCreerEvenement_DateFinInvalide() { + // Given + evenementTest.setDateFin(evenementTest.getDateDebut().minusHours(1)); + + // When & Then + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, () -> evenementService.creerEvenement(evenementTest)); + + assertEquals( + "La date de fin ne peut pas être antérieure à la date de début", exception.getMessage()); + } + + @Test + @Order(6) + @DisplayName("Mise à jour d'événement - Succès") + void testMettreAJourEvenement_Succes() { + // Given + when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest)); + when(keycloakService.hasRole("ADMIN")).thenReturn(true); + when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com"); + doNothing().when(evenementRepository).persist(any(Evenement.class)); + + Evenement evenementMisAJour = + Evenement.builder() + .titre("Assemblée Générale 2025 - Modifiée") + .description("Description mise à jour") + .dateDebut(LocalDateTime.now().plusDays(35)) + .dateFin(LocalDateTime.now().plusDays(35).plusHours(4)) + .lieu("Nouvelle salle") + .typeEvenement(TypeEvenement.ASSEMBLEE_GENERALE) + .capaciteMax(150) + .prix(BigDecimal.valueOf(30.00)) + .inscriptionRequise(true) + .visiblePublic(true) + .build(); + + // When + Evenement resultat = evenementService.mettreAJourEvenement(1L, evenementMisAJour); + + // Then + assertNotNull(resultat); + assertEquals("Assemblée Générale 2025 - Modifiée", resultat.getTitre()); + assertEquals("Description mise à jour", resultat.getDescription()); + assertEquals("Nouvelle salle", resultat.getLieu()); + assertEquals(150, resultat.getCapaciteMax()); + assertEquals(BigDecimal.valueOf(30.00), resultat.getPrix()); + assertEquals("admin@test.com", resultat.getModifiePar()); + + verify(evenementRepository).persist(any(Evenement.class)); + } + + @Test + @Order(7) + @DisplayName("Mise à jour d'événement - Événement non trouvé") + void testMettreAJourEvenement_NonTrouve() { + // Given + when(evenementRepository.findByIdOptional(999L)).thenReturn(Optional.empty()); + + // When & Then + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> evenementService.mettreAJourEvenement(999L, evenementTest)); + + assertEquals("Événement non trouvé avec l'ID: 999", exception.getMessage()); + } + + @Test + @Order(8) + @DisplayName("Suppression d'événement - Succès") + void testSupprimerEvenement_Succes() { + // Given + when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest)); + when(keycloakService.hasRole("ADMIN")).thenReturn(true); + when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com"); + when(evenementTest.getNombreInscrits()).thenReturn(0); + doNothing().when(evenementRepository).persist(any(Evenement.class)); + + // When + assertDoesNotThrow(() -> evenementService.supprimerEvenement(1L)); + + // Then + assertFalse(evenementTest.getActif()); + assertEquals("admin@test.com", evenementTest.getModifiePar()); + verify(evenementRepository).persist(any(Evenement.class)); + } + + @Test + @Order(9) + @DisplayName("Recherche d'événements - Succès") + void testRechercherEvenements_Succes() { + // Given + List evenementsAttendus = List.of(evenementTest); + when(evenementRepository.findByTitreOrDescription( + anyString(), any(Page.class), any(Sort.class))) + .thenReturn(evenementsAttendus); + + // When + List resultat = + evenementService.rechercherEvenements("Assemblée", Page.of(0, 10), Sort.by("dateDebut")); + + // Then + assertNotNull(resultat); + assertEquals(1, resultat.size()); + assertEquals("Assemblée Générale 2025", resultat.get(0).getTitre()); + + verify(evenementRepository) + .findByTitreOrDescription(eq("Assemblée"), any(Page.class), any(Sort.class)); + } + + @Test + @Order(10) + @DisplayName("Changement de statut - Succès") + void testChangerStatut_Succes() { + // Given + when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest)); + when(keycloakService.hasRole("ADMIN")).thenReturn(true); + when(keycloakService.getCurrentUserEmail()).thenReturn("admin@test.com"); + doNothing().when(evenementRepository).persist(any(Evenement.class)); + + // When + Evenement resultat = evenementService.changerStatut(1L, StatutEvenement.CONFIRME); + + // Then + assertNotNull(resultat); + assertEquals(StatutEvenement.CONFIRME, resultat.getStatut()); + assertEquals("admin@test.com", resultat.getModifiePar()); + + verify(evenementRepository).persist(any(Evenement.class)); + } + + @Test + @Order(11) + @DisplayName("Statistiques des événements") + void testObtenirStatistiques() { + // Given + Map statsBase = + Map.of( + "total", 100L, + "actifs", 80L, + "aVenir", 30L, + "enCours", 5L, + "passes", 45L, + "publics", 70L, + "avecInscription", 25L); + when(evenementRepository.getStatistiques()).thenReturn(statsBase); + + // When + Map resultat = evenementService.obtenirStatistiques(); + + // Then + assertNotNull(resultat); + assertEquals(100L, resultat.get("total")); + assertEquals(80L, resultat.get("actifs")); + assertEquals(30L, resultat.get("aVenir")); + assertEquals(80.0, resultat.get("tauxActivite")); + assertEquals(37.5, resultat.get("tauxEvenementsAVenir")); + assertEquals(6.25, resultat.get("tauxEvenementsEnCours")); + assertNotNull(resultat.get("timestamp")); + + verify(evenementRepository).getStatistiques(); + } + + @Test + @Order(12) + @DisplayName("Lister événements actifs avec pagination") + void testListerEvenementsActifs() { + // Given + List evenementsAttendus = List.of(evenementTest); + when(evenementRepository.findAllActifs(any(Page.class), any(Sort.class))) + .thenReturn(evenementsAttendus); + + // When + List resultat = + evenementService.listerEvenementsActifs(Page.of(0, 20), Sort.by("dateDebut")); + + // Then + assertNotNull(resultat); + assertEquals(1, resultat.size()); + assertEquals("Assemblée Générale 2025", resultat.get(0).getTitre()); + + verify(evenementRepository).findAllActifs(any(Page.class), any(Sort.class)); + } + + @Test + @Order(13) + @DisplayName("Validation des règles métier - Prix négatif") + void testValidation_PrixNegatif() { + // Given + evenementTest.setPrix(BigDecimal.valueOf(-10.00)); + + // When & Then + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, () -> evenementService.creerEvenement(evenementTest)); + + assertEquals("Le prix ne peut pas être négatif", exception.getMessage()); + } + + @Test + @Order(14) + @DisplayName("Validation des règles métier - Capacité négative") + void testValidation_CapaciteNegative() { + // Given + evenementTest.setCapaciteMax(-5); + + // When & Then + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, () -> evenementService.creerEvenement(evenementTest)); + + assertEquals("La capacité maximale ne peut pas être négative", exception.getMessage()); + } + + @Test + @Order(15) + @DisplayName("Permissions - Utilisateur non autorisé") + void testPermissions_UtilisateurNonAutorise() { + // Given + when(evenementRepository.findByIdOptional(1L)).thenReturn(Optional.of(evenementTest)); + when(keycloakService.hasRole(anyString())).thenReturn(false); + when(keycloakService.getCurrentUserEmail()).thenReturn("autre@test.com"); + + // When & Then + SecurityException exception = + assertThrows( + SecurityException.class, + () -> evenementService.mettreAJourEvenement(1L, evenementTest)); + + assertEquals( + "Vous n'avez pas les permissions pour modifier cet événement", exception.getMessage()); + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/service/MembreServiceTest.java b/src/test.bak/java/dev/lions/unionflow/server/service/MembreServiceTest.java new file mode 100644 index 0000000..6d2b884 --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/service/MembreServiceTest.java @@ -0,0 +1,344 @@ +package dev.lions.unionflow.server.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.repository.MembreRepository; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * Tests pour MembreService + * + * @author Lions Dev Team + * @since 2025-01-10 + */ +@ExtendWith(MockitoExtension.class) +@DisplayName("Tests MembreService") +class MembreServiceTest { + + @InjectMocks MembreService membreService; + + @Mock MembreRepository membreRepository; + + private Membre membreTest; + + @BeforeEach + void setUp() { + membreTest = + Membre.builder() + .prenom("Jean") + .nom("Dupont") + .email("jean.dupont@test.com") + .telephone("221701234567") + .dateNaissance(LocalDate.of(1990, 5, 15)) + .dateAdhesion(LocalDate.now()) + .actif(true) + .build(); + } + + @Nested + @DisplayName("Tests creerMembre") + class CreerMembreTests { + + @Test + @DisplayName("Création réussie d'un membre") + void testCreerMembreReussi() { + // Given + when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(membreRepository.findByNumeroMembre(anyString())).thenReturn(Optional.empty()); + + // When + Membre result = membreService.creerMembre(membreTest); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getNumeroMembre()).isNotNull(); + assertThat(result.getNumeroMembre()).startsWith("UF2025-"); + verify(membreRepository).persist(membreTest); + } + + @Test + @DisplayName("Erreur si email déjà existant") + void testCreerMembreEmailExistant() { + // Given + when(membreRepository.findByEmail(membreTest.getEmail())).thenReturn(Optional.of(membreTest)); + + // When & Then + assertThatThrownBy(() -> membreService.creerMembre(membreTest)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Un membre avec cet email existe déjà"); + } + + @Test + @DisplayName("Erreur si numéro de membre déjà existant") + void testCreerMembreNumeroExistant() { + // Given + membreTest.setNumeroMembre("UF2025-EXIST"); + when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(membreRepository.findByNumeroMembre("UF2025-EXIST")).thenReturn(Optional.of(membreTest)); + + // When & Then + assertThatThrownBy(() -> membreService.creerMembre(membreTest)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Un membre avec ce numéro existe déjà"); + } + + @Test + @DisplayName("Génération automatique du numéro de membre") + void testGenerationNumeroMembre() { + // Given + membreTest.setNumeroMembre(null); + when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(membreRepository.findByNumeroMembre(anyString())).thenReturn(Optional.empty()); + + // When + membreService.creerMembre(membreTest); + + // Then + ArgumentCaptor captor = ArgumentCaptor.forClass(Membre.class); + verify(membreRepository).persist(captor.capture()); + assertThat(captor.getValue().getNumeroMembre()).isNotNull(); + assertThat(captor.getValue().getNumeroMembre()).startsWith("UF2025-"); + } + + @Test + @DisplayName("Génération automatique du numéro de membre avec chaîne vide") + void testGenerationNumeroMembreChainVide() { + // Given + membreTest.setNumeroMembre(""); // Chaîne vide + when(membreRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(membreRepository.findByNumeroMembre(anyString())).thenReturn(Optional.empty()); + + // When + membreService.creerMembre(membreTest); + + // Then + ArgumentCaptor captor = ArgumentCaptor.forClass(Membre.class); + verify(membreRepository).persist(captor.capture()); + assertThat(captor.getValue().getNumeroMembre()).isNotNull(); + assertThat(captor.getValue().getNumeroMembre()).isNotEmpty(); + assertThat(captor.getValue().getNumeroMembre()).startsWith("UF2025-"); + } + } + + @Nested + @DisplayName("Tests mettreAJourMembre") + class MettreAJourMembreTests { + + @Test + @DisplayName("Mise à jour réussie d'un membre") + void testMettreAJourMembreReussi() { + // Given + Long id = 1L; + membreTest.id = id; // Utiliser le champ directement + Membre membreModifie = + Membre.builder() + .prenom("Pierre") + .nom("Martin") + .email("pierre.martin@test.com") + .telephone("221701234568") + .dateNaissance(LocalDate.of(1985, 8, 20)) + .actif(false) + .build(); + + when(membreRepository.findById(id)).thenReturn(membreTest); + when(membreRepository.findByEmail("pierre.martin@test.com")).thenReturn(Optional.empty()); + + // When + Membre result = membreService.mettreAJourMembre(id, membreModifie); + + // Then + assertThat(result.getPrenom()).isEqualTo("Pierre"); + assertThat(result.getNom()).isEqualTo("Martin"); + assertThat(result.getEmail()).isEqualTo("pierre.martin@test.com"); + assertThat(result.getTelephone()).isEqualTo("221701234568"); + assertThat(result.getDateNaissance()).isEqualTo(LocalDate.of(1985, 8, 20)); + assertThat(result.getActif()).isFalse(); + } + + @Test + @DisplayName("Erreur si membre non trouvé") + void testMettreAJourMembreNonTrouve() { + // Given + Long id = 999L; + when(membreRepository.findById(id)).thenReturn(null); + + // When & Then + assertThatThrownBy(() -> membreService.mettreAJourMembre(id, membreTest)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Membre non trouvé avec l'ID: " + id); + } + + @Test + @DisplayName("Erreur si nouvel email déjà existant") + void testMettreAJourMembreEmailExistant() { + // Given + Long id = 1L; + membreTest.id = id; // Utiliser le champ directement + membreTest.setEmail("ancien@test.com"); + + Membre membreModifie = Membre.builder().email("nouveau@test.com").build(); + + Membre autreMembreAvecEmail = Membre.builder().email("nouveau@test.com").build(); + autreMembreAvecEmail.id = 2L; // Utiliser le champ directement + + when(membreRepository.findById(id)).thenReturn(membreTest); + when(membreRepository.findByEmail("nouveau@test.com")) + .thenReturn(Optional.of(autreMembreAvecEmail)); + + // When & Then + assertThatThrownBy(() -> membreService.mettreAJourMembre(id, membreModifie)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Un membre avec cet email existe déjà"); + } + + @Test + @DisplayName("Mise à jour sans changement d'email") + void testMettreAJourMembreSansChangementEmail() { + // Given + Long id = 1L; + membreTest.id = id; // Utiliser le champ directement + membreTest.setEmail("meme@test.com"); + + Membre membreModifie = + Membre.builder() + .prenom("Pierre") + .nom("Martin") + .email("meme@test.com") // Même email + .telephone("221701234568") + .dateNaissance(LocalDate.of(1985, 8, 20)) + .actif(false) + .build(); + + when(membreRepository.findById(id)).thenReturn(membreTest); + // Pas besoin de mocker findByEmail car l'email n'a pas changé + + // When + Membre result = membreService.mettreAJourMembre(id, membreModifie); + + // Then + assertThat(result.getPrenom()).isEqualTo("Pierre"); + assertThat(result.getNom()).isEqualTo("Martin"); + assertThat(result.getEmail()).isEqualTo("meme@test.com"); + // Vérifier que findByEmail n'a pas été appelé + verify(membreRepository, never()).findByEmail("meme@test.com"); + } + } + + @Test + @DisplayName("Test trouverParId") + void testTrouverParId() { + // Given + Long id = 1L; + when(membreRepository.findById(id)).thenReturn(membreTest); + + // When + Optional result = membreService.trouverParId(id); + + // Then + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(membreTest); + } + + @Test + @DisplayName("Test trouverParEmail") + void testTrouverParEmail() { + // Given + String email = "jean.dupont@test.com"; + when(membreRepository.findByEmail(email)).thenReturn(Optional.of(membreTest)); + + // When + Optional result = membreService.trouverParEmail(email); + + // Then + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(membreTest); + } + + @Test + @DisplayName("Test listerMembresActifs") + void testListerMembresActifs() { + // Given + List membresActifs = Arrays.asList(membreTest); + when(membreRepository.findAllActifs()).thenReturn(membresActifs); + + // When + List result = membreService.listerMembresActifs(); + + // Then + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(membreTest); + } + + @Test + @DisplayName("Test rechercherMembres") + void testRechercherMembres() { + // Given + String recherche = "Jean"; + List resultatsRecherche = Arrays.asList(membreTest); + when(membreRepository.findByNomOrPrenom(recherche)).thenReturn(resultatsRecherche); + + // When + List result = membreService.rechercherMembres(recherche); + + // Then + assertThat(result).hasSize(1); + assertThat(result.get(0)).isEqualTo(membreTest); + } + + @Test + @DisplayName("Test desactiverMembre - Succès") + void testDesactiverMembreReussi() { + // Given + Long id = 1L; + membreTest.id = id; // Utiliser le champ directement + when(membreRepository.findById(id)).thenReturn(membreTest); + + // When + membreService.desactiverMembre(id); + + // Then + assertThat(membreTest.getActif()).isFalse(); + } + + @Test + @DisplayName("Test desactiverMembre - Membre non trouvé") + void testDesactiverMembreNonTrouve() { + // Given + Long id = 999L; + when(membreRepository.findById(id)).thenReturn(null); + + // When & Then + assertThatThrownBy(() -> membreService.desactiverMembre(id)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Membre non trouvé avec l'ID: " + id); + } + + @Test + @DisplayName("Test compterMembresActifs") + void testCompterMembresActifs() { + // Given + when(membreRepository.countActifs()).thenReturn(5L); + + // When + long result = membreService.compterMembresActifs(); + + // Then + assertThat(result).isEqualTo(5L); + } +} diff --git a/src/test.bak/java/dev/lions/unionflow/server/service/OrganisationServiceTest.java b/src/test.bak/java/dev/lions/unionflow/server/service/OrganisationServiceTest.java new file mode 100644 index 0000000..0d15e6f --- /dev/null +++ b/src/test.bak/java/dev/lions/unionflow/server/service/OrganisationServiceTest.java @@ -0,0 +1,356 @@ +package dev.lions.unionflow.server.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +/** + * Tests unitaires pour OrganisationService + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-15 + */ +@QuarkusTest +class OrganisationServiceTest { + + @Inject OrganisationService organisationService; + + @Mock OrganisationRepository organisationRepository; + + private Organisation organisationTest; + + @BeforeEach + void setUp() { + organisationTest = + Organisation.builder() + .nom("Lions Club Test") + .nomCourt("LC Test") + .email("test@lionsclub.org") + .typeOrganisation("LIONS_CLUB") + .statut("ACTIVE") + .description("Organisation de test") + .telephone("+225 01 02 03 04 05") + .adresse("123 Rue de Test") + .ville("Abidjan") + .region("Lagunes") + .pays("Côte d'Ivoire") + .nombreMembres(25) + .actif(true) + .dateCreation(LocalDateTime.now()) + .version(0L) + .build(); + organisationTest.id = 1L; + } + + @Test + void testCreerOrganisation_Success() { + // Given + Organisation organisationToCreate = + Organisation.builder() + .nom("Lions Club Test New") + .email("testnew@lionsclub.org") + .typeOrganisation("LIONS_CLUB") + .build(); + + when(organisationRepository.findByEmail("testnew@lionsclub.org")).thenReturn(Optional.empty()); + when(organisationRepository.findByNom("Lions Club Test New")).thenReturn(Optional.empty()); + + // When + Organisation result = organisationService.creerOrganisation(organisationToCreate); + + // Then + assertNotNull(result); + assertEquals("Lions Club Test New", result.getNom()); + assertEquals("ACTIVE", result.getStatut()); + verify(organisationRepository).findByEmail("testnew@lionsclub.org"); + verify(organisationRepository).findByNom("Lions Club Test New"); + } + + @Test + void testCreerOrganisation_EmailDejaExistant() { + // Given + when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.of(organisationTest)); + + // When & Then + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> organisationService.creerOrganisation(organisationTest)); + + assertEquals("Une organisation avec cet email existe déjà", exception.getMessage()); + verify(organisationRepository).findByEmail("test@lionsclub.org"); + verify(organisationRepository, never()).findByNom(anyString()); + } + + @Test + void testCreerOrganisation_NomDejaExistant() { + // Given + when(organisationRepository.findByEmail(anyString())).thenReturn(Optional.empty()); + when(organisationRepository.findByNom(anyString())).thenReturn(Optional.of(organisationTest)); + + // When & Then + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> organisationService.creerOrganisation(organisationTest)); + + assertEquals("Une organisation avec ce nom existe déjà", exception.getMessage()); + verify(organisationRepository).findByEmail("test@lionsclub.org"); + verify(organisationRepository).findByNom("Lions Club Test"); + } + + @Test + void testMettreAJourOrganisation_Success() { + // Given + Organisation organisationMiseAJour = + Organisation.builder() + .nom("Lions Club Test Modifié") + .email("test@lionsclub.org") + .description("Description modifiée") + .telephone("+225 01 02 03 04 06") + .build(); + + when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest)); + when(organisationRepository.findByNom("Lions Club Test Modifié")).thenReturn(Optional.empty()); + + // When + Organisation result = + organisationService.mettreAJourOrganisation(1L, organisationMiseAJour, "testUser"); + + // Then + assertNotNull(result); + assertEquals("Lions Club Test Modifié", result.getNom()); + assertEquals("Description modifiée", result.getDescription()); + assertEquals("+225 01 02 03 04 06", result.getTelephone()); + assertEquals("testUser", result.getModifiePar()); + assertNotNull(result.getDateModification()); + assertEquals(1L, result.getVersion()); + } + + @Test + void testMettreAJourOrganisation_OrganisationNonTrouvee() { + // Given + when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.empty()); + + // When & Then + NotFoundException exception = + assertThrows( + NotFoundException.class, + () -> organisationService.mettreAJourOrganisation(1L, organisationTest, "testUser")); + + assertEquals("Organisation non trouvée avec l'ID: 1", exception.getMessage()); + } + + @Test + void testSupprimerOrganisation_Success() { + // Given + organisationTest.setNombreMembres(0); + when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest)); + + // When + organisationService.supprimerOrganisation(1L, "testUser"); + + // Then + assertFalse(organisationTest.getActif()); + assertEquals("DISSOUTE", organisationTest.getStatut()); + assertEquals("testUser", organisationTest.getModifiePar()); + assertNotNull(organisationTest.getDateModification()); + } + + @Test + void testSupprimerOrganisation_AvecMembresActifs() { + // Given + organisationTest.setNombreMembres(5); + when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest)); + + // When & Then + IllegalStateException exception = + assertThrows( + IllegalStateException.class, + () -> organisationService.supprimerOrganisation(1L, "testUser")); + + assertEquals( + "Impossible de supprimer une organisation avec des membres actifs", exception.getMessage()); + } + + @Test + void testTrouverParId_Success() { + // Given + when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest)); + + // When + Optional result = organisationService.trouverParId(1L); + + // Then + assertTrue(result.isPresent()); + assertEquals("Lions Club Test", result.get().getNom()); + verify(organisationRepository).findByIdOptional(1L); + } + + @Test + void testTrouverParId_NonTrouve() { + // Given + when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.empty()); + + // When + Optional result = organisationService.trouverParId(1L); + + // Then + assertFalse(result.isPresent()); + verify(organisationRepository).findByIdOptional(1L); + } + + @Test + void testTrouverParEmail_Success() { + // Given + when(organisationRepository.findByEmail("test@lionsclub.org")) + .thenReturn(Optional.of(organisationTest)); + + // When + Optional result = organisationService.trouverParEmail("test@lionsclub.org"); + + // Then + assertTrue(result.isPresent()); + assertEquals("Lions Club Test", result.get().getNom()); + verify(organisationRepository).findByEmail("test@lionsclub.org"); + } + + @Test + void testListerOrganisationsActives() { + // Given + List organisations = Arrays.asList(organisationTest); + when(organisationRepository.findAllActives()).thenReturn(organisations); + + // When + List result = organisationService.listerOrganisationsActives(); + + // Then + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("Lions Club Test", result.get(0).getNom()); + verify(organisationRepository).findAllActives(); + } + + @Test + void testActiverOrganisation_Success() { + // Given + organisationTest.setStatut("SUSPENDUE"); + organisationTest.setActif(false); + when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest)); + + // When + Organisation result = organisationService.activerOrganisation(1L, "testUser"); + + // Then + assertNotNull(result); + assertEquals("ACTIVE", result.getStatut()); + assertTrue(result.getActif()); + assertEquals("testUser", result.getModifiePar()); + assertNotNull(result.getDateModification()); + } + + @Test + void testSuspendreOrganisation_Success() { + // Given + when(organisationRepository.findByIdOptional(1L)).thenReturn(Optional.of(organisationTest)); + + // When + Organisation result = organisationService.suspendreOrganisation(1L, "testUser"); + + // Then + assertNotNull(result); + assertEquals("SUSPENDUE", result.getStatut()); + assertFalse(result.getAccepteNouveauxMembres()); + assertEquals("testUser", result.getModifiePar()); + assertNotNull(result.getDateModification()); + } + + @Test + void testObtenirStatistiques() { + // Given + when(organisationRepository.count()).thenReturn(100L); + when(organisationRepository.countActives()).thenReturn(85L); + when(organisationRepository.countNouvellesOrganisations(any(LocalDate.class))).thenReturn(5L); + + // When + Map result = organisationService.obtenirStatistiques(); + + // Then + assertNotNull(result); + assertEquals(100L, result.get("totalOrganisations")); + assertEquals(85L, result.get("organisationsActives")); + assertEquals(15L, result.get("organisationsInactives")); + assertEquals(5L, result.get("nouvellesOrganisations30Jours")); + assertEquals(85.0, result.get("tauxActivite")); + assertNotNull(result.get("timestamp")); + } + + @Test + void testConvertToDTO() { + // When + var dto = organisationService.convertToDTO(organisationTest); + + // Then + assertNotNull(dto); + assertEquals("Lions Club Test", dto.getNom()); + assertEquals("LC Test", dto.getNomCourt()); + assertEquals("test@lionsclub.org", dto.getEmail()); + assertEquals("Organisation de test", dto.getDescription()); + assertEquals("+225 01 02 03 04 05", dto.getTelephone()); + assertEquals("Abidjan", dto.getVille()); + assertEquals(25, dto.getNombreMembres()); + assertTrue(dto.getActif()); + } + + @Test + void testConvertToDTO_Null() { + // When + var dto = organisationService.convertToDTO(null); + + // Then + assertNull(dto); + } + + @Test + void testConvertFromDTO() { + // Given + var dto = organisationService.convertToDTO(organisationTest); + + // When + Organisation result = organisationService.convertFromDTO(dto); + + // Then + assertNotNull(result); + assertEquals("Lions Club Test", result.getNom()); + assertEquals("LC Test", result.getNomCourt()); + assertEquals("test@lionsclub.org", result.getEmail()); + assertEquals("Organisation de test", result.getDescription()); + assertEquals("+225 01 02 03 04 05", result.getTelephone()); + assertEquals("Abidjan", result.getVille()); + } + + @Test + void testConvertFromDTO_Null() { + // When + Organisation result = organisationService.convertFromDTO(null); + + // Then + assertNull(result); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/resource/EvenementResourceTest.java b/src/test/java/dev/lions/unionflow/server/resource/EvenementResourceTest.java new file mode 100644 index 0000000..c220305 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/resource/EvenementResourceTest.java @@ -0,0 +1,400 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +import dev.lions.unionflow.server.entity.Evenement; +import dev.lions.unionflow.server.entity.Evenement.StatutEvenement; +import dev.lions.unionflow.server.entity.Evenement.TypeEvenement; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.EvenementRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; +import org.junit.jupiter.api.*; + +/** + * Tests d'intégration pour EvenementResource + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-19 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class EvenementResourceTest { + + private static final String BASE_PATH = "/api/evenements"; + + @Inject EvenementRepository evenementRepository; + @Inject OrganisationRepository organisationRepository; + + private Evenement testEvenement; + private Organisation testOrganisation; + + @BeforeEach + @Transactional + void setupTestData() { + // Créer une organisation de test + testOrganisation = + Organisation.builder() + .nom("Organisation Test Événements") + .typeOrganisation("ASSOCIATION") + .statut("ACTIF") + .email("org-events-" + System.currentTimeMillis() + "@test.com") + .build(); + testOrganisation.setDateCreation(LocalDateTime.now()); + testOrganisation.setActif(true); + organisationRepository.persist(testOrganisation); + + // Créer un événement de test + testEvenement = + Evenement.builder() + .titre("Événement Test") + .description("Description de l'événement de test") + .dateDebut(LocalDateTime.now().plusDays(7)) + .dateFin(LocalDateTime.now().plusDays(7).plusHours(3)) + .lieu("Lieu Test") + .typeEvenement(TypeEvenement.REUNION) + .statut(StatutEvenement.PLANIFIE) + .capaciteMax(50) + .prix(BigDecimal.valueOf(5000)) + .organisation(testOrganisation) + .build(); + testEvenement.setDateCreation(LocalDateTime.now()); + testEvenement.setActif(true); + evenementRepository.persist(testEvenement); + } + + @AfterEach + @Transactional + void cleanupTestData() { + // Supprimer tous les événements liés à l'organisation avant de supprimer l'organisation + if (testOrganisation != null && testOrganisation.getId() != null) { + // Supprimer tous les événements de cette organisation + java.util.List evenements = + evenementRepository.findByOrganisation(testOrganisation.getId()); + for (Evenement evt : evenements) { + evenementRepository.delete(evt); + } + } + + // Supprimer l'événement de test s'il existe encore + if (testEvenement != null && testEvenement.getId() != null) { + Evenement evtToDelete = evenementRepository.findById(testEvenement.getId()); + if (evtToDelete != null) { + evenementRepository.delete(evtToDelete); + } + } + + // Supprimer l'organisation après avoir supprimé tous ses événements + if (testOrganisation != null && testOrganisation.getId() != null) { + Organisation orgToDelete = organisationRepository.findById(testOrganisation.getId()); + if (orgToDelete != null) { + organisationRepository.delete(orgToDelete); + } + } + } + + @Test + @Order(1) + @DisplayName("GET /api/evenements/test doit retourner un statut de connectivité") + void testConnectivity() { + given() + .when() + .get(BASE_PATH + "/test") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("status", equalTo("success")) + .body("message", notNullValue()); + } + + @Test + @Order(2) + @DisplayName("GET /api/evenements/count doit retourner le nombre d'événements") + void testCountEvenements() { + given() + .when() + .get(BASE_PATH + "/count") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("count", greaterThanOrEqualTo(0)) + .body("status", equalTo("success")); + } + + @Test + @Order(3) + @DisplayName("GET /api/evenements doit retourner la liste des événements") + void testListerEvenements() { + given() + .queryParam("page", 0) + .queryParam("size", 20) + .when() + .get(BASE_PATH) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("$", notNullValue()); + } + + @Test + @Order(4) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/evenements/{id} doit retourner un événement existant") + void testObtenirEvenement() { + UUID eventId = testEvenement.getId(); + + given() + .pathParam("id", eventId) + .when() + .get(BASE_PATH + "/{id}") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("id", equalTo(eventId.toString())) + .body("titre", equalTo("Événement Test")); + } + + @Test + @Order(5) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/evenements/{id} doit retourner 404 pour un ID inexistant") + void testObtenirEvenementInexistant() { + UUID fakeId = UUID.randomUUID(); + + given() + .pathParam("id", fakeId) + .when() + .get(BASE_PATH + "/{id}") + .then() + .statusCode(404); + } + + @Test + @Order(6) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("POST /api/evenements doit créer un nouvel événement") + void testCreerEvenement() { + String eventJson = + """ + { + "titre": "Nouvel Événement Test", + "description": "Description du nouvel événement", + "dateDebut": "%s", + "dateFin": "%s", + "lieu": "Nouveau Lieu", + "typeEvenement": "REUNION", + "statut": "PLANIFIE", + "capaciteMax": 100, + "prix": 10000, + "organisationId": "%s" + } + """ + .formatted( + LocalDateTime.now().plusDays(14).toString(), + LocalDateTime.now().plusDays(14).plusHours(4).toString(), + testOrganisation.getId().toString()); + + UUID createdId = + UUID.fromString( + given() + .contentType(ContentType.JSON) + .body(eventJson) + .when() + .post(BASE_PATH) + .then() + .statusCode(201) + .contentType(ContentType.JSON) + .body("titre", equalTo("Nouvel Événement Test")) + .body("id", notNullValue()) + .extract() + .path("id")); + + // Nettoyer l'événement créé + Evenement created = evenementRepository.findById(createdId); + if (created != null) { + evenementRepository.delete(created); + } + } + + @Test + @Order(7) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("POST /api/evenements doit retourner 400 pour données invalides") + void testCreerEvenementInvalide() { + String invalidEventJson = + """ + { + "titre": "", + "description": "Description", + "dateDebut": "%s" + } + """ + .formatted(LocalDateTime.now().plusDays(1).toString()); + + given() + .contentType(ContentType.JSON) + .body(invalidEventJson) + .when() + .post(BASE_PATH) + .then() + .statusCode(400); + } + + @Test + @Order(8) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("PUT /api/evenements/{id} doit mettre à jour un événement") + void testModifierEvenement() { + UUID eventId = testEvenement.getId(); + // Récupérer l'événement existant pour préserver l'organisation + Evenement existing = evenementRepository.findById(eventId); + String updatedEventJson = + """ + { + "titre": "Événement Modifié", + "description": "Description modifiée", + "dateDebut": "%s", + "dateFin": "%s", + "lieu": "Lieu Modifié", + "typeEvenement": "REUNION", + "statut": "PLANIFIE", + "capaciteMax": 75, + "prix": 7500, + "actif": true, + "visiblePublic": true, + "inscriptionRequise": false + } + """ + .formatted( + LocalDateTime.now().plusDays(10).toString(), + LocalDateTime.now().plusDays(10).plusHours(5).toString()); + + given() + .contentType(ContentType.JSON) + .pathParam("id", eventId) + .body(updatedEventJson) + .when() + .put(BASE_PATH + "/{id}") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("titre", equalTo("Événement Modifié")) + .body("lieu", equalTo("Lieu Modifié")); + } + + @Test + @Order(9) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("PUT /api/evenements/{id} doit retourner 404 pour ID inexistant") + void testModifierEvenementInexistant() { + UUID fakeId = UUID.randomUUID(); + String updatedEventJson = + """ + { + "titre": "Événement Test", + "dateDebut": "%s", + "actif": true, + "visiblePublic": true, + "inscriptionRequise": false + } + """ + .formatted(LocalDateTime.now().plusDays(1).toString()); + + given() + .contentType(ContentType.JSON) + .pathParam("id", fakeId) + .body(updatedEventJson) + .when() + .put(BASE_PATH + "/{id}") + .then() + .statusCode(404); + } + + @Test + @Order(10) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("DELETE /api/evenements/{id} doit supprimer un événement") + void testSupprimerEvenement() { + // Créer un événement temporaire pour la suppression + Evenement tempEvent = + Evenement.builder() + .titre("Événement à Supprimer") + .description("Description") + .dateDebut(LocalDateTime.now().plusDays(5)) + .typeEvenement(TypeEvenement.REUNION) + .statut(StatutEvenement.PLANIFIE) + .organisation(testOrganisation) + .build(); + tempEvent.setDateCreation(LocalDateTime.now()); + tempEvent.setActif(true); + evenementRepository.persist(tempEvent); + + UUID tempId = tempEvent.getId(); + + given() + .pathParam("id", tempId) + .when() + .delete(BASE_PATH + "/{id}") + .then() + .statusCode(204); + + // Vérifier que l'événement a été supprimé + Evenement deleted = evenementRepository.findById(tempId); + assert deleted == null : "L'événement devrait être supprimé"; + } + + @Test + @Order(11) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("DELETE /api/evenements/{id} doit retourner 404 pour ID inexistant") + void testSupprimerEvenementInexistant() { + UUID fakeId = UUID.randomUUID(); + + given() + .pathParam("id", fakeId) + .when() + .delete(BASE_PATH + "/{id}") + .then() + .statusCode(404); + } + + @Test + @Order(12) + @DisplayName("GET /api/evenements doit supporter la pagination") + void testPagination() { + given() + .queryParam("page", 0) + .queryParam("size", 5) + .when() + .get(BASE_PATH) + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + @Test + @Order(13) + @DisplayName("GET /api/evenements doit supporter le tri") + void testTri() { + given() + .queryParam("page", 0) + .queryParam("size", 10) + .queryParam("sort", "dateDebut") + .queryParam("direction", "desc") + .when() + .get(BASE_PATH) + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } +} + diff --git a/src/test/java/dev/lions/unionflow/server/resource/MembreResourceAdvancedSearchTest.java b/src/test/java/dev/lions/unionflow/server/resource/MembreResourceAdvancedSearchTest.java new file mode 100644 index 0000000..1aeca82 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/resource/MembreResourceAdvancedSearchTest.java @@ -0,0 +1,334 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.*; + +/** + * Tests d'intégration pour l'endpoint de recherche avancée des membres + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-19 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MembreResourceAdvancedSearchTest { + + private static final String ADVANCED_SEARCH_ENDPOINT = "/api/membres/search/advanced"; + + @Test + @Order(1) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit fonctionner avec critères valides") + void testAdvancedSearchWithValidCriteria() { + MembreSearchCriteria criteria = + MembreSearchCriteria.builder().query("marie").statut("ACTIF").ageMin(20).ageMax(50).build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .queryParam("page", 0) + .queryParam("size", 20) + .queryParam("sort", "nom") + .queryParam("direction", "asc") + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("membres", notNullValue()) + .body("totalElements", greaterThanOrEqualTo(0)) + .body("totalPages", greaterThanOrEqualTo(0)) + .body("currentPage", equalTo(0)) + .body("pageSize", equalTo(20)) + .body("hasNext", notNullValue()) + .body("hasPrevious", equalTo(false)) + .body("isFirst", equalTo(true)) + .body("executionTimeMs", greaterThan(0)) + .body("statistics", notNullValue()) + .body("statistics.membresActifs", greaterThanOrEqualTo(0)) + .body("statistics.membresInactifs", greaterThanOrEqualTo(0)) + .body("criteria.query", equalTo("marie")) + .body("criteria.statut", equalTo("ACTIF")); + } + + @Test + @Order(2) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit fonctionner avec critères multiples") + void testAdvancedSearchWithMultipleCriteria() { + MembreSearchCriteria criteria = + MembreSearchCriteria.builder() + .email("@unionflow.com") + .dateAdhesionMin(LocalDate.of(2020, 1, 1)) + .dateAdhesionMax(LocalDate.of(2025, 12, 31)) + .roles(List.of("ADMIN", "SUPER_ADMIN")) + .includeInactifs(false) + .build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .queryParam("page", 0) + .queryParam("size", 10) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("membres", notNullValue()) + .body("totalElements", greaterThanOrEqualTo(0)) + .body("criteria.email", equalTo("@unionflow.com")) + .body("criteria.roles", hasItems("ADMIN", "SUPER_ADMIN")) + .body("criteria.includeInactifs", equalTo(false)); + } + + @Test + @Order(3) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit gérer la pagination") + void testAdvancedSearchPagination() { + MembreSearchCriteria criteria = + MembreSearchCriteria.builder() + .statut("ACTIF") // Ajouter un critère valide + .includeInactifs(true) // Inclure tous les membres + .build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .queryParam("page", 0) + .queryParam("size", 2) // Petite taille pour tester la pagination + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("currentPage", equalTo(0)) + .body("pageSize", equalTo(2)) + .body("isFirst", equalTo(true)) + .body("hasPrevious", equalTo(false)); + } + + @Test + @Order(4) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit gérer le tri") + void testAdvancedSearchSorting() { + MembreSearchCriteria criteria = MembreSearchCriteria.builder().statut("ACTIF").build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .queryParam("sort", "nom") + .queryParam("direction", "desc") + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("membres", notNullValue()); + } + + @Test + @Order(5) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit retourner 400 pour critères vides") + void testAdvancedSearchWithEmptyCriteria() { + MembreSearchCriteria emptyCriteria = MembreSearchCriteria.builder().build(); + + given() + .contentType(ContentType.JSON) + .body(emptyCriteria) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(400) + .contentType(ContentType.JSON) + .body("message", containsString("Au moins un critère de recherche doit être spécifié")); + } + + @Test + @Order(6) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit retourner 400 pour critères invalides") + void testAdvancedSearchWithInvalidCriteria() { + MembreSearchCriteria invalidCriteria = + MembreSearchCriteria.builder() + .ageMin(50) + .ageMax(30) // Âge max < âge min + .build(); + + given() + .contentType(ContentType.JSON) + .body(invalidCriteria) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(400) + .contentType(ContentType.JSON) + .body("message", containsString("Critères de recherche invalides")); + } + + @Test + @Order(7) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit retourner 400 pour body null") + void testAdvancedSearchWithNullBody() { + given() + .contentType(ContentType.JSON) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(400); + } + + @Test + @Order(8) + @TestSecurity( + user = "marie.active@unionflow.com", + roles = {"MEMBRE_ACTIF"}) + @DisplayName("POST /api/membres/search/advanced doit retourner 403 pour utilisateur non autorisé") + void testAdvancedSearchUnauthorized() { + MembreSearchCriteria criteria = MembreSearchCriteria.builder().query("test").build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(403); + } + + @Test + @Order(9) + @DisplayName( + "POST /api/membres/search/advanced doit retourner 401 pour utilisateur non authentifié") + void testAdvancedSearchUnauthenticated() { + MembreSearchCriteria criteria = MembreSearchCriteria.builder().query("test").build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(401); + } + + @Test + @Order(10) + @TestSecurity( + user = "admin@unionflow.com", + roles = {"ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit fonctionner pour ADMIN") + void testAdvancedSearchForAdmin() { + MembreSearchCriteria criteria = MembreSearchCriteria.builder().statut("ACTIF").build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("membres", notNullValue()); + } + + @Test + @Order(11) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit inclure le temps d'exécution") + void testAdvancedSearchExecutionTime() { + MembreSearchCriteria criteria = MembreSearchCriteria.builder().query("test").build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("executionTimeMs", greaterThan(0)) + .body("executionTimeMs", lessThan(5000)); // Moins de 5 secondes + } + + @Test + @Order(12) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit retourner des statistiques complètes") + void testAdvancedSearchStatistics() { + MembreSearchCriteria criteria = + MembreSearchCriteria.builder() + .statut("ACTIF") // Ajouter un critère valide + .includeInactifs(true) + .build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("statistics", notNullValue()) + .body("statistics.membresActifs", greaterThanOrEqualTo(0)) + .body("statistics.membresInactifs", greaterThanOrEqualTo(0)) + .body("statistics.ageMoyen", greaterThanOrEqualTo(0.0)) + .body("statistics.ageMin", greaterThanOrEqualTo(0)) + .body("statistics.ageMax", greaterThanOrEqualTo(0)) + .body("statistics.nombreOrganisations", greaterThanOrEqualTo(0)) + .body("statistics.ancienneteMoyenne", greaterThanOrEqualTo(0.0)); + } + + @Test + @Order(13) + @TestSecurity( + user = "superadmin@unionflow.com", + roles = {"SUPER_ADMIN"}) + @DisplayName("POST /api/membres/search/advanced doit gérer les caractères spéciaux") + void testAdvancedSearchWithSpecialCharacters() { + MembreSearchCriteria criteria = + MembreSearchCriteria.builder().query("marie-josé").nom("o'connor").build(); + + given() + .contentType(ContentType.JSON) + .body(criteria) + .when() + .post(ADVANCED_SEARCH_ENDPOINT) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("membres", notNullValue()); + } +} diff --git a/src/test/java/dev/lions/unionflow/server/resource/MembreResourceImportExportTest.java b/src/test/java/dev/lions/unionflow/server/resource/MembreResourceImportExportTest.java new file mode 100644 index 0000000..0368d77 --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/resource/MembreResourceImportExportTest.java @@ -0,0 +1,292 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.io.ByteArrayOutputStream; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.*; + +/** + * Tests d'intégration pour les endpoints import/export de MembreResource + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-19 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MembreResourceImportExportTest { + + private static final String BASE_PATH = "/api/membres"; + + @Inject MembreRepository membreRepository; + @Inject OrganisationRepository organisationRepository; + + private Organisation testOrganisation; + private List testMembres; + + @BeforeEach + @Transactional + void setupTestData() { + // Créer une organisation de test + testOrganisation = + Organisation.builder() + .nom("Organisation Test Import/Export") + .typeOrganisation("ASSOCIATION") + .statut("ACTIF") + .email("org-import-export-" + System.currentTimeMillis() + "@test.com") + .build(); + testOrganisation.setDateCreation(LocalDateTime.now()); + testOrganisation.setActif(true); + organisationRepository.persist(testOrganisation); + + // Créer quelques membres de test + testMembres = new ArrayList<>(); + for (int i = 1; i <= 3; i++) { + Membre membre = + Membre.builder() + .numeroMembre("UF-TEST-IMPORT-" + i) + .nom("Nom" + i) + .prenom("Prenom" + i) + .email("membre" + i + "-import-" + System.currentTimeMillis() + "@test.com") + .telephone("+22170123456" + i) + .dateNaissance(LocalDate.of(1990 + i, 1, 1)) + .dateAdhesion(LocalDate.of(2023, 1, 1)) + .organisation(testOrganisation) + .build(); + membre.setDateCreation(LocalDateTime.now()); + membre.setActif(true); + membreRepository.persist(membre); + testMembres.add(membre); + } + } + + @AfterEach + @Transactional + void cleanupTestData() { + if (testMembres != null) { + testMembres.forEach( + membre -> { + if (membre.getId() != null) { + Membre m = membreRepository.findById(membre.getId()); + if (m != null) { + membreRepository.delete(m); + } + } + }); + } + + if (testOrganisation != null && testOrganisation.getId() != null) { + Organisation org = organisationRepository.findById(testOrganisation.getId()); + if (org != null) { + organisationRepository.delete(org); + } + } + } + + @Test + @Order(1) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/membres/import/modele doit retourner un fichier Excel template") + void testTelechargerModeleImport() { + given() + .when() + .get(BASE_PATH + "/import/modele") + .then() + .statusCode(200) + .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + .header("Content-Disposition", containsString("attachment")) + .header("Content-Disposition", containsString("modele_import_membres")); + } + + @Test + @Order(2) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("POST /api/membres/import doit importer des membres depuis Excel") + void testImporterMembresExcel() throws Exception { + // Créer un fichier Excel de test + byte[] excelFile = createTestExcelFile(); + + given() + .contentType("multipart/form-data") + .multiPart("file", "test_import.xlsx", excelFile, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + .formParam("organisationId", testOrganisation.getId().toString()) + .formParam("typeMembreDefaut", "ACTIF") + .formParam("mettreAJourExistants", "false") + .formParam("ignorerErreurs", "false") + .when() + .post(BASE_PATH + "/import") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("lignesTraitees", greaterThan(0)) + .body("erreurs", notNullValue()); + } + + @Test + @Order(3) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("POST /api/membres/import doit retourner 400 pour fichier vide") + void testImporterMembresFichierVide() { + // Envoyer un fichier vide (tableau de bytes vide) + byte[] emptyFile = new byte[0]; + + given() + .contentType("multipart/form-data") + .multiPart("file", "empty.xlsx", emptyFile, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + .formParam("organisationId", testOrganisation.getId().toString()) + .when() + .post(BASE_PATH + "/import") + .then() + .statusCode(400); + } + + @Test + @Order(4) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/membres/export doit exporter des membres en Excel") + void testExporterMembresExcel() { + given() + .queryParam("format", "EXCEL") + .queryParam("statut", "ACTIF") + .when() + .get(BASE_PATH + "/export") + .then() + .statusCode(200) + .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + .header("Content-Disposition", containsString("attachment")); + } + + @Test + @Order(5) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/membres/export doit exporter en CSV") + void testExporterMembresCSV() { + given() + .queryParam("format", "CSV") + .queryParam("statut", "ACTIF") + .when() + .get(BASE_PATH + "/export") + .then() + .statusCode(200) + .contentType("text/csv") + .header("Content-Disposition", containsString("attachment")); + } + + @Test + @Order(6) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/membres/export/count doit retourner le nombre de membres à exporter") + void testCompterMembresPourExport() { + given() + .queryParam("statut", "ACTIF") + .when() + .get(BASE_PATH + "/export/count") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("count", greaterThanOrEqualTo(0)); + } + + @Test + @Order(7) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("POST /api/membres/export/selection doit exporter une sélection de membres") + void testExporterSelectionMembres() { + List membreIds = new ArrayList<>(); + testMembres.forEach(m -> membreIds.add(m.getId())); + + given() + .contentType(ContentType.JSON) + .body(membreIds) + .queryParam("format", "EXCEL") + .when() + .post(BASE_PATH + "/export/selection") + .then() + .statusCode(200) + .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + .header("Content-Disposition", containsString("attachment")); + } + + @Test + @Order(8) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/membres/export doit supporter inclureStatistiques") + void testExporterAvecStatistiques() { + given() + .queryParam("format", "EXCEL") + .queryParam("inclureStatistiques", "true") + .queryParam("statut", "ACTIF") + .when() + .get(BASE_PATH + "/export") + .then() + .statusCode(200) + .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + } + + @Test + @Order(9) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/membres/export doit supporter le chiffrement") + void testExporterAvecChiffrement() { + given() + .queryParam("format", "EXCEL") + .queryParam("motDePasse", "testPassword123") + .queryParam("statut", "ACTIF") + .when() + .get(BASE_PATH + "/export") + .then() + .statusCode(200) + .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + } + + /** + * Crée un fichier Excel de test avec des données de membres valides + */ + private byte[] createTestExcelFile() throws Exception { + try (Workbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + + Sheet sheet = workbook.createSheet("Membres"); + + // En-têtes + Row headerRow = sheet.createRow(0); + String[] headers = { + "nom", "prenom", "email", "telephone", "dateNaissance", "dateAdhesion" + }; + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + } + + // Données de test + Row dataRow = sheet.createRow(1); + dataRow.createCell(0).setCellValue("TestNom"); + dataRow.createCell(1).setCellValue("TestPrenom"); + dataRow.createCell(2).setCellValue("test-import-" + System.currentTimeMillis() + "@test.com"); + dataRow.createCell(3).setCellValue("+221701234999"); + dataRow.createCell(4).setCellValue("1990-01-01"); + dataRow.createCell(5).setCellValue("2023-01-01"); + + workbook.write(out); + return out.toByteArray(); + } + } +} + diff --git a/src/test/java/dev/lions/unionflow/server/resource/OrganisationResourceTest.java b/src/test/java/dev/lions/unionflow/server/resource/OrganisationResourceTest.java new file mode 100644 index 0000000..14df72f --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/resource/OrganisationResourceTest.java @@ -0,0 +1,351 @@ +package dev.lions.unionflow.server.resource; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +import dev.lions.unionflow.server.api.dto.organisation.OrganisationDTO; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.UUID; +import org.junit.jupiter.api.*; + +/** + * Tests d'intégration pour OrganisationResource + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-19 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class OrganisationResourceTest { + + private static final String BASE_PATH = "/api/organisations"; + + @Inject OrganisationRepository organisationRepository; + + private Organisation testOrganisation; + + @BeforeEach + @Transactional + void setupTestData() { + // Créer une organisation de test + testOrganisation = + Organisation.builder() + .nom("Organisation Test") + .typeOrganisation("ASSOCIATION") + .statut("ACTIF") + .email("test-org-" + System.currentTimeMillis() + "@test.com") + .telephone("+221701234567") + .ville("Dakar") + .pays("Sénégal") + .build(); + testOrganisation.setDateCreation(LocalDateTime.now()); + testOrganisation.setActif(true); + organisationRepository.persist(testOrganisation); + } + + @AfterEach + @Transactional + void cleanupTestData() { + if (testOrganisation != null && testOrganisation.getId() != null) { + Organisation orgToDelete = organisationRepository.findById(testOrganisation.getId()); + if (orgToDelete != null) { + organisationRepository.delete(orgToDelete); + } + } + } + + @Test + @Order(1) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/organisations doit retourner la liste des organisations") + void testListerOrganisations() { + given() + .when() + .get(BASE_PATH) + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("$", notNullValue()); + } + + @Test + @Order(2) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/organisations/{id} doit retourner une organisation existante") + void testObtenirOrganisation() { + UUID orgId = testOrganisation.getId(); + + given() + .pathParam("id", orgId) + .when() + .get(BASE_PATH + "/{id}") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("id", equalTo(orgId.toString())) + .body("nom", equalTo("Organisation Test")); + } + + @Test + @Order(3) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("GET /api/organisations/{id} doit retourner 404 pour un ID inexistant") + void testObtenirOrganisationInexistante() { + UUID fakeId = UUID.randomUUID(); + + given() + .pathParam("id", fakeId) + .when() + .get(BASE_PATH + "/{id}") + .then() + .statusCode(404); + } + + @Test + @Order(4) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("POST /api/organisations doit créer une nouvelle organisation") + void testCreerOrganisation() { + OrganisationDTO newOrg = new OrganisationDTO(); + newOrg.setNom("Nouvelle Organisation Test"); + newOrg.setTypeOrganisation(dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation.ASSOCIATION); + newOrg.setStatut(dev.lions.unionflow.server.api.enums.organisation.StatutOrganisation.ACTIVE); + newOrg.setEmail("nouvelle-org-" + System.currentTimeMillis() + "@test.com"); + newOrg.setTelephone("+221701234568"); + newOrg.setVille("Thiès"); + newOrg.setPays("Sénégal"); + + String location = + given() + .contentType(ContentType.JSON) + .body(newOrg) + .when() + .post(BASE_PATH) + .then() + .statusCode(201) + .contentType(ContentType.JSON) + .body("nom", equalTo("Nouvelle Organisation Test")) + .body("id", notNullValue()) + .extract() + .header("Location"); + + // Nettoyer l'organisation créée + if (location != null && location.contains("/")) { + String idStr = location.substring(location.lastIndexOf("/") + 1); + try { + UUID createdId = UUID.fromString(idStr); + Organisation created = organisationRepository.findById(createdId); + if (created != null) { + organisationRepository.delete(created); + } + } catch (Exception e) { + // Ignorer les erreurs de nettoyage + } + } + } + + @Test + @Order(5) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("POST /api/organisations doit retourner 400 pour données invalides") + void testCreerOrganisationInvalide() { + OrganisationDTO invalidOrg = new OrganisationDTO(); + invalidOrg.setNom(""); // Nom vide - invalide + invalidOrg.setTypeOrganisation(dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation.ASSOCIATION); + invalidOrg.setStatut(dev.lions.unionflow.server.api.enums.organisation.StatutOrganisation.ACTIVE); + invalidOrg.setEmail("invalid-email"); // Email invalide + + given() + .contentType(ContentType.JSON) + .body(invalidOrg) + .when() + .post(BASE_PATH) + .then() + .statusCode(400); + } + + @Test + @Order(6) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("POST /api/organisations doit retourner 409 pour email dupliqué") + void testCreerOrganisationEmailDuplique() { + OrganisationDTO duplicateOrg = new OrganisationDTO(); + duplicateOrg.setNom("Autre Organisation"); + duplicateOrg.setTypeOrganisation(dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation.ASSOCIATION); + duplicateOrg.setStatut(dev.lions.unionflow.server.api.enums.organisation.StatutOrganisation.ACTIVE); + duplicateOrg.setEmail(testOrganisation.getEmail()); // Email déjà utilisé + + given() + .contentType(ContentType.JSON) + .body(duplicateOrg) + .when() + .post(BASE_PATH) + .then() + .statusCode(409); + } + + @Test + @Order(7) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("PUT /api/organisations/{id} doit mettre à jour une organisation") + void testModifierOrganisation() { + UUID orgId = testOrganisation.getId(); + OrganisationDTO updatedOrg = new OrganisationDTO(); + updatedOrg.setNom("Organisation Modifiée"); + updatedOrg.setTypeOrganisation(dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation.ASSOCIATION); + updatedOrg.setStatut(dev.lions.unionflow.server.api.enums.organisation.StatutOrganisation.ACTIVE); + updatedOrg.setEmail(testOrganisation.getEmail()); + updatedOrg.setTelephone("+221701234999"); + updatedOrg.setVille("Saint-Louis"); + updatedOrg.setPays("Sénégal"); + + given() + .contentType(ContentType.JSON) + .pathParam("id", orgId) + .body(updatedOrg) + .when() + .put(BASE_PATH + "/{id}") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("nom", equalTo("Organisation Modifiée")) + .body("telephone", equalTo("+221701234999")); + } + + @Test + @Order(8) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("PUT /api/organisations/{id} doit retourner 404 pour ID inexistant") + void testModifierOrganisationInexistante() { + UUID fakeId = UUID.randomUUID(); + OrganisationDTO updatedOrg = new OrganisationDTO(); + updatedOrg.setNom("Organisation Test"); + updatedOrg.setTypeOrganisation(dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation.ASSOCIATION); + updatedOrg.setStatut(dev.lions.unionflow.server.api.enums.organisation.StatutOrganisation.ACTIVE); + updatedOrg.setEmail("fake-" + System.currentTimeMillis() + "@test.com"); + + given() + .contentType(ContentType.JSON) + .pathParam("id", fakeId) + .body(updatedOrg) + .when() + .put(BASE_PATH + "/{id}") + .then() + .statusCode(404); + } + + @Test + @Order(9) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("DELETE /api/organisations/{id} doit supprimer une organisation") + void testSupprimerOrganisation() { + // Créer une organisation temporaire pour la suppression + Organisation tempOrg = + Organisation.builder() + .nom("Organisation à Supprimer") + .typeOrganisation("ASSOCIATION") + .statut("ACTIF") + .email("temp-delete-" + System.currentTimeMillis() + "@test.com") + .build(); + tempOrg.setDateCreation(LocalDateTime.now()); + tempOrg.setActif(true); + organisationRepository.persist(tempOrg); + + UUID tempId = tempOrg.getId(); + + given() + .pathParam("id", tempId) + .when() + .delete(BASE_PATH + "/{id}") + .then() + .statusCode(204); + + // Vérifier que l'organisation a été supprimée + Organisation deleted = organisationRepository.findById(tempId); + assert deleted == null : "L'organisation devrait être supprimée"; + } + + @Test + @Order(10) + @TestSecurity(user = "admin@unionflow.com", roles = {"ADMIN"}) + @DisplayName("DELETE /api/organisations/{id} doit retourner 404 pour ID inexistant") + void testSupprimerOrganisationInexistante() { + UUID fakeId = UUID.randomUUID(); + + given() + .pathParam("id", fakeId) + .when() + .delete(BASE_PATH + "/{id}") + .then() + .statusCode(404); + } + + @Test + @Order(11) + @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) + @DisplayName("GET /api/organisations doit être accessible aux membres") + void testListerOrganisationsPourMembre() { + given() + .when() + .get(BASE_PATH) + .then() + .statusCode(200); + } + + @Test + @Order(12) + @DisplayName("GET /api/organisations doit être accessible publiquement") + void testListerOrganisationsPublic() { + given() + .when() + .get(BASE_PATH) + .then() + .statusCode(200); + } + + @Test + @Order(13) + @TestSecurity(user = "membre@unionflow.com", roles = {"MEMBRE"}) + @DisplayName("POST /api/organisations doit être accessible aux membres") + void testCreerOrganisationPourMembre() { + OrganisationDTO newOrg = new OrganisationDTO(); + newOrg.setNom("Organisation Créée par Membre"); + newOrg.setTypeOrganisation(dev.lions.unionflow.server.api.enums.organisation.TypeOrganisation.ASSOCIATION); + newOrg.setStatut(dev.lions.unionflow.server.api.enums.organisation.StatutOrganisation.ACTIVE); + newOrg.setEmail("membre-org-" + System.currentTimeMillis() + "@test.com"); + + String location = + given() + .contentType(ContentType.JSON) + .body(newOrg) + .when() + .post(BASE_PATH) + .then() + .statusCode(201) + .extract() + .header("Location"); + + // Nettoyer + if (location != null && location.contains("/")) { + String idStr = location.substring(location.lastIndexOf("/") + 1); + try { + UUID createdId = UUID.fromString(idStr); + Organisation created = organisationRepository.findById(createdId); + if (created != null) { + organisationRepository.delete(created); + } + } catch (Exception e) { + // Ignorer + } + } + } +} + diff --git a/src/test/java/dev/lions/unionflow/server/service/MembreImportExportServiceTest.java b/src/test/java/dev/lions/unionflow/server/service/MembreImportExportServiceTest.java new file mode 100644 index 0000000..261093f --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/service/MembreImportExportServiceTest.java @@ -0,0 +1,369 @@ +package dev.lions.unionflow.server.service; + +import static org.assertj.core.api.Assertions.*; + +import dev.lions.unionflow.server.api.dto.membre.MembreDTO; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.*; + +/** + * Tests unitaires pour MembreImportExportService + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-19 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MembreImportExportServiceTest { + + @Inject MembreImportExportService importExportService; + @Inject MembreRepository membreRepository; + @Inject OrganisationRepository organisationRepository; + @Inject MembreService membreService; + + private Organisation testOrganisation; + private List testMembres; + + @BeforeEach + @Transactional + void setupTestData() { + // Créer une organisation de test + testOrganisation = + Organisation.builder() + .nom("Organisation Test Import/Export Service") + .typeOrganisation("ASSOCIATION") + .statut("ACTIF") + .email("org-service-" + System.currentTimeMillis() + "@test.com") + .build(); + testOrganisation.setDateCreation(LocalDateTime.now()); + testOrganisation.setActif(true); + organisationRepository.persist(testOrganisation); + + // Créer quelques membres de test + testMembres = new ArrayList<>(); + for (int i = 1; i <= 5; i++) { + Membre membre = + Membre.builder() + .numeroMembre("UF-SERVICE-TEST-" + i) + .nom("NomService" + i) + .prenom("PrenomService" + i) + .email("service" + i + "-" + System.currentTimeMillis() + "@test.com") + .telephone("+2217012345" + (10 + i)) + .dateNaissance(LocalDate.of(1985 + i, 1, 1)) + .dateAdhesion(LocalDate.of(2022, 1, 1)) + .organisation(testOrganisation) + .build(); + membre.setDateCreation(LocalDateTime.now()); + membre.setActif(true); + membreRepository.persist(membre); + testMembres.add(membre); + } + } + + @AfterEach + @Transactional + void cleanupTestData() { + if (testMembres != null) { + testMembres.forEach( + membre -> { + if (membre.getId() != null) { + Membre m = membreRepository.findById(membre.getId()); + if (m != null) { + membreRepository.delete(m); + } + } + }); + } + + if (testOrganisation != null && testOrganisation.getId() != null) { + Organisation org = organisationRepository.findById(testOrganisation.getId()); + if (org != null) { + organisationRepository.delete(org); + } + } + } + + @Test + @Order(1) + @DisplayName("Doit générer un modèle d'import Excel valide") + void testGenererModeleImport() throws Exception { + // When + byte[] modele = importExportService.genererModeleImport(); + + // Then + assertThat(modele).isNotNull(); + assertThat(modele.length).isGreaterThan(0); + + // Vérifier que c'est un fichier Excel valide + try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(modele))) { + Sheet sheet = workbook.getSheetAt(0); + assertThat(sheet).isNotNull(); + Row headerRow = sheet.getRow(0); + assertThat(headerRow).isNotNull(); + // Vérifier la présence de colonnes essentielles + boolean hasNom = false, hasPrenom = false, hasEmail = false; + for (Cell cell : headerRow) { + String value = cell.getStringCellValue().toLowerCase(); + if (value.contains("nom")) hasNom = true; + if (value.contains("prenom")) hasPrenom = true; + if (value.contains("email")) hasEmail = true; + } + assertThat(hasNom).isTrue(); + assertThat(hasPrenom).isTrue(); + assertThat(hasEmail).isTrue(); + } + } + + @Test + @Order(2) + @DisplayName("Doit importer des membres depuis un fichier Excel valide") + void testImporterDepuisExcel() throws Exception { + // Given - Créer un fichier Excel de test + byte[] excelFile = createValidExcelFile(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(excelFile); + + // When + MembreImportExportService.ResultatImport resultat = + importExportService.importerMembres( + inputStream, + "test_import.xlsx", + testOrganisation.getId(), + "ACTIF", + false, + false); + + // Then + assertThat(resultat).isNotNull(); + assertThat(resultat.lignesTraitees).isGreaterThan(0); + assertThat(resultat.membresImportes).isNotEmpty(); + assertThat(resultat.erreurs).isEmpty(); + } + + @Test + @Order(3) + @DisplayName("Doit gérer les erreurs lors de l'import Excel") + void testImporterExcelAvecErreurs() throws Exception { + // Given - Créer un fichier Excel avec des données invalides + byte[] excelFile = createInvalidExcelFile(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(excelFile); + + // When + MembreImportExportService.ResultatImport resultat = + importExportService.importerMembres( + inputStream, + "test_invalid.xlsx", + testOrganisation.getId(), + "ACTIF", + false, + true); // Ignorer les erreurs + + // Then + assertThat(resultat).isNotNull(); + assertThat(resultat.erreurs).isNotEmpty(); + } + + @Test + @Order(4) + @DisplayName("Doit exporter des membres vers Excel") + void testExporterVersExcel() throws Exception { + // Given - Convertir les membres de test en DTOs + List membresDTO = new ArrayList<>(); + testMembres.forEach(m -> membresDTO.add(membreService.convertToDTO(m))); + + // When + byte[] excelData = + importExportService.exporterVersExcel( + membresDTO, + List.of("nom", "prenom", "email", "telephone"), + true, // inclureHeaders + false, // formaterDates + false, // inclureStatistiques + null); // motDePasse + + // Then + assertThat(excelData).isNotNull(); + assertThat(excelData.length).isGreaterThan(0); + + // Vérifier que c'est un fichier Excel valide + try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(excelData))) { + Sheet sheet = workbook.getSheetAt(0); + assertThat(sheet).isNotNull(); + assertThat(sheet.getLastRowNum()).isGreaterThan(0); + } + } + + @Test + @Order(5) + @DisplayName("Doit exporter des membres vers Excel avec statistiques") + void testExporterVersExcelAvecStatistiques() throws Exception { + // Given + List membresDTO = new ArrayList<>(); + testMembres.forEach(m -> membresDTO.add(membreService.convertToDTO(m))); + + // When + byte[] excelData = + importExportService.exporterVersExcel( + membresDTO, + List.of("nom", "prenom", "email"), + true, // inclureHeaders + false, // formaterDates + true, // inclureStatistiques + null); // motDePasse + + // Then + assertThat(excelData).isNotNull(); + + // Vérifier qu'il y a plusieurs feuilles (données + statistiques) + try (Workbook workbook = new XSSFWorkbook(new ByteArrayInputStream(excelData))) { + assertThat(workbook.getNumberOfSheets()).isGreaterThan(1); + Sheet statsSheet = workbook.getSheet("Statistiques"); + assertThat(statsSheet).isNotNull(); + } + } + + @Test + @Order(6) + @DisplayName("Doit exporter des membres vers Excel avec chiffrement") + void testExporterVersExcelAvecChiffrement() throws Exception { + // Given + List membresDTO = new ArrayList<>(); + testMembres.forEach(m -> membresDTO.add(membreService.convertToDTO(m))); + + // When + byte[] excelData = + importExportService.exporterVersExcel( + membresDTO, + List.of("nom", "prenom", "email"), + true, // inclureHeaders + false, // formaterDates + false, // inclureStatistiques + "testPassword123"); // motDePasse + + // Then + assertThat(excelData).isNotNull(); + // Note: La vérification du chiffrement nécessiterait d'essayer d'ouvrir le fichier avec le mot de passe + } + + @Test + @Order(7) + @DisplayName("Doit exporter des membres vers CSV") + void testExporterVersCSV() throws Exception { + // Given + List membresDTO = new ArrayList<>(); + testMembres.forEach(m -> membresDTO.add(membreService.convertToDTO(m))); + + // When - Utiliser les groupes de colonnes attendus par la méthode + byte[] csvData = + importExportService.exporterVersCSV( + membresDTO, + List.of("PERSO", "CONTACT"), // Groupes de colonnes + true, // inclureHeaders + false); // formaterDates + + // Then + assertThat(csvData).isNotNull(); + assertThat(csvData.length).isGreaterThan(0); + + // Vérifier le contenu CSV - les en-têtes sont en français avec majuscules + String csvContent = new String(csvData, java.nio.charset.StandardCharsets.UTF_8); + assertThat(csvContent).contains("Nom"); + assertThat(csvContent).contains("Prénom"); + assertThat(csvContent).contains("Email"); + } + + @Test + @Order(8) + @DisplayName("Doit rejeter un format de fichier non supporté") + void testFormatNonSupporte() { + // Given + byte[] invalidFile = "Ceci n'est pas un fichier Excel".getBytes(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(invalidFile); + + // When & Then + assertThatThrownBy( + () -> + importExportService.importerMembres( + inputStream, + "test.txt", + testOrganisation.getId(), + "ACTIF", + false, + false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Format de fichier non supporté"); + } + + /** + * Crée un fichier Excel valide pour les tests d'import + */ + private byte[] createValidExcelFile() throws Exception { + try (Workbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + + Sheet sheet = workbook.createSheet("Membres"); + + // En-têtes + Row headerRow = sheet.createRow(0); + String[] headers = { + "nom", "prenom", "email", "telephone", "dateNaissance", "dateAdhesion" + }; + for (int i = 0; i < headers.length; i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers[i]); + } + + // Données valides + Row dataRow = sheet.createRow(1); + dataRow.createCell(0).setCellValue("TestNom"); + dataRow.createCell(1).setCellValue("TestPrenom"); + dataRow.createCell(2).setCellValue("test-valid-" + System.currentTimeMillis() + "@test.com"); + dataRow.createCell(3).setCellValue("+221701234999"); + dataRow.createCell(4).setCellValue("1990-01-01"); + dataRow.createCell(5).setCellValue("2023-01-01"); + + workbook.write(out); + return out.toByteArray(); + } + } + + /** + * Crée un fichier Excel avec des données invalides pour tester la gestion d'erreurs + */ + private byte[] createInvalidExcelFile() throws Exception { + try (Workbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + + Sheet sheet = workbook.createSheet("Membres"); + + // En-têtes + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("nom"); + headerRow.createCell(1).setCellValue("prenom"); + headerRow.createCell(2).setCellValue("email"); + + // Données invalides (email manquant) + Row dataRow = sheet.createRow(1); + dataRow.createCell(0).setCellValue("TestNom"); + dataRow.createCell(1).setCellValue("TestPrenom"); + // Email manquant - devrait générer une erreur + + workbook.write(out); + return out.toByteArray(); + } + } +} diff --git a/src/test/java/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.java b/src/test/java/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.java new file mode 100644 index 0000000..4d1eb4c --- /dev/null +++ b/src/test/java/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.java @@ -0,0 +1,400 @@ +package dev.lions.unionflow.server.service; + +import static org.assertj.core.api.Assertions.*; + +import dev.lions.unionflow.server.api.dto.membre.MembreSearchCriteria; +import dev.lions.unionflow.server.api.dto.membre.MembreSearchResultDTO; +import dev.lions.unionflow.server.entity.Membre; +import dev.lions.unionflow.server.entity.Organisation; +import dev.lions.unionflow.server.repository.MembreRepository; +import dev.lions.unionflow.server.repository.OrganisationRepository; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.*; + +/** + * Tests pour la recherche avancée de membres + * + * @author UnionFlow Team + * @version 1.0 + * @since 2025-01-19 + */ +@QuarkusTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MembreServiceAdvancedSearchTest { + + @Inject MembreService membreService; + @Inject MembreRepository membreRepository; + @Inject OrganisationRepository organisationRepository; + + private Organisation testOrganisation; + private List testMembres; + + @BeforeEach + @Transactional + void setupTestData() { + // Créer et persister une organisation de test + testOrganisation = + Organisation.builder() + .nom("Organisation Test") + .typeOrganisation("ASSOCIATION") + .statut("ACTIF") + .email("test@organisation.com") + .build(); + testOrganisation.setDateCreation(LocalDateTime.now()); + testOrganisation.setActif(true); + organisationRepository.persist(testOrganisation); + + // Créer des membres de test avec différents profils + testMembres = + List.of( + // Membre actif jeune + createMembre("UF-2025-TEST001", "Dupont", "Marie", "marie.dupont@test.com", + "+221701234567", LocalDate.of(1995, 5, 15), LocalDate.of(2023, 1, 15), + "MEMBRE,SECRETAIRE", true), + + // Membre actif âgé + createMembre("UF-2025-TEST002", "Martin", "Jean", "jean.martin@test.com", + "+221701234568", LocalDate.of(1970, 8, 20), LocalDate.of(2020, 3, 10), + "MEMBRE,PRESIDENT", true), + + // Membre inactif + createMembre("UF-2025-TEST003", "Diallo", "Fatou", "fatou.diallo@test.com", + "+221701234569", LocalDate.of(1985, 12, 3), LocalDate.of(2021, 6, 5), + "MEMBRE", false), + + // Membre avec email spécifique + createMembre("UF-2025-TEST004", "Sow", "Amadou", "amadou.sow@unionflow.com", + "+221701234570", LocalDate.of(1988, 3, 12), LocalDate.of(2022, 9, 20), + "MEMBRE,TRESORIER", true)); + + // Persister tous les membres + testMembres.forEach(membre -> membreRepository.persist(membre)); + } + + private Membre createMembre(String numero, String nom, String prenom, String email, + String telephone, LocalDate dateNaissance, LocalDate dateAdhesion, + String roles, boolean actif) { + Membre membre = Membre.builder() + .numeroMembre(numero) + .nom(nom) + .prenom(prenom) + .email(email) + .telephone(telephone) + .dateNaissance(dateNaissance) + .dateAdhesion(dateAdhesion) + .organisation(testOrganisation) + .build(); + membre.setDateCreation(LocalDateTime.now()); + membre.setActif(actif); + // Note: Le champ roles est maintenant List et doit être géré via la relation MembreRole + // Pour les tests, on laisse la liste vide par défaut + return membre; + } + + @AfterEach + @Transactional + void cleanupTestData() { + // Nettoyer les données de test + if (testMembres != null) { + testMembres.forEach(membre -> { + if (membre.getId() != null) { + // Recharger l'entité depuis la base pour éviter l'erreur "detached entity" + membreRepository.findByIdOptional(membre.getId()).ifPresent(m -> { + // Utiliser deleteById pour éviter les problèmes avec les entités détachées + membreRepository.deleteById(m.getId()); + }); + } + }); + } + + if (testOrganisation != null && testOrganisation.getId() != null) { + // Recharger l'entité depuis la base pour éviter l'erreur "detached entity" + organisationRepository.findByIdOptional(testOrganisation.getId()).ifPresent(o -> { + // Utiliser deleteById pour éviter les problèmes avec les entités détachées + organisationRepository.deleteById(o.getId()); + }); + } + } + + @Test + @Order(1) + @DisplayName("Doit effectuer une recherche par terme général") + void testSearchByGeneralQuery() { + // Given + MembreSearchCriteria criteria = MembreSearchCriteria.builder().query("marie").build(); + + // When + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getMembres()).hasSize(1); + assertThat(result.getMembres().get(0).getPrenom()).isEqualToIgnoringCase("Marie"); + assertThat(result.isFirst()).isTrue(); + assertThat(result.isLast()).isTrue(); + } + + @Test + @Order(2) + @DisplayName("Doit filtrer par statut actif") + void testSearchByActiveStatus() { + // Given + MembreSearchCriteria criteria = MembreSearchCriteria.builder().statut("ACTIF").build(); + + // When + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTotalElements()).isEqualTo(3); // 3 membres actifs + assertThat(result.getMembres()).hasSize(3); + assertThat(result.getMembres()).allMatch(membre -> "ACTIF".equals(membre.getStatut())); + } + + @Test + @Order(3) + @DisplayName("Doit filtrer par tranche d'âge") + void testSearchByAgeRange() { + // Given + MembreSearchCriteria criteria = MembreSearchCriteria.builder().ageMin(25).ageMax(35).build(); + + // When + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTotalElements()).isGreaterThan(0); + + // Vérifier que tous les membres sont dans la tranche d'âge + result + .getMembres() + .forEach( + membre -> { + if (membre.getDateNaissance() != null) { + int age = LocalDate.now().getYear() - membre.getDateNaissance().getYear(); + assertThat(age).isBetween(25, 35); + } + }); + } + + @Test + @Order(4) + @DisplayName("Doit filtrer par période d'adhésion") + void testSearchByAdhesionPeriod() { + // Given + MembreSearchCriteria criteria = + MembreSearchCriteria.builder() + .dateAdhesionMin(LocalDate.of(2022, 1, 1)) + .dateAdhesionMax(LocalDate.of(2023, 12, 31)) + .build(); + + // When + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("dateAdhesion")); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTotalElements()).isGreaterThan(0); + + // Vérifier que toutes les dates d'adhésion sont dans la période + result + .getMembres() + .forEach( + membre -> { + if (membre.getDateAdhesion() != null) { + assertThat(membre.getDateAdhesion()) + .isAfterOrEqualTo(LocalDate.of(2022, 1, 1)) + .isBeforeOrEqualTo(LocalDate.of(2023, 12, 31)); + } + }); + } + + @Test + @Order(5) + @DisplayName("Doit rechercher par email avec domaine spécifique") + void testSearchByEmailDomain() { + // Given + MembreSearchCriteria criteria = MembreSearchCriteria.builder().email("@unionflow.com").build(); + + // When + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getMembres()).hasSize(1); + assertThat(result.getMembres().get(0).getEmail()).contains("@unionflow.com"); + } + + @Test + @Order(6) + @DisplayName("Doit filtrer par rôles") + void testSearchByRoles() { + // Given + MembreSearchCriteria criteria = + MembreSearchCriteria.builder().roles(List.of("PRESIDENT", "SECRETAIRE")).build(); + + // When + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTotalElements()).isGreaterThan(0); + + // Vérifier que tous les membres ont au moins un des rôles recherchés + result + .getMembres() + .forEach( + membre -> { + assertThat(membre.getRole()) + .satisfiesAnyOf( + role -> assertThat(role).contains("PRESIDENT"), + role -> assertThat(role).contains("SECRETAIRE")); + }); + } + + @Test + @Order(7) + @DisplayName("Doit gérer la pagination correctement") + void testPagination() { + // Given + MembreSearchCriteria criteria = + MembreSearchCriteria.builder() + .includeInactifs(true) // Inclure tous les membres + .build(); + + // When - Première page + MembreSearchResultDTO firstPage = + membreService.searchMembresAdvanced(criteria, Page.of(0, 2), Sort.by("nom")); + + // Then + assertThat(firstPage).isNotNull(); + assertThat(firstPage.getCurrentPage()).isEqualTo(0); + assertThat(firstPage.getPageSize()).isEqualTo(2); + assertThat(firstPage.getMembres()).hasSizeLessThanOrEqualTo(2); + assertThat(firstPage.isFirst()).isTrue(); + + if (firstPage.getTotalElements() > 2) { + assertThat(firstPage.isLast()).isFalse(); + assertThat(firstPage.isHasNext()).isTrue(); + } + } + + @Test + @Order(8) + @DisplayName("Doit calculer les statistiques correctement") + void testStatisticsCalculation() { + // Given + MembreSearchCriteria criteria = + MembreSearchCriteria.builder() + .includeInactifs(true) // Inclure tous les membres + .build(); + + // When + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getStatistics()).isNotNull(); + + MembreSearchResultDTO.SearchStatistics stats = result.getStatistics(); + assertThat(stats.getMembresActifs()).isEqualTo(3); + assertThat(stats.getMembresInactifs()).isEqualTo(1); + assertThat(stats.getAgeMoyen()).isGreaterThan(0); + assertThat(stats.getAgeMin()).isGreaterThan(0); + assertThat(stats.getAgeMax()).isGreaterThan(stats.getAgeMin()); + assertThat(stats.getAncienneteMoyenne()).isGreaterThanOrEqualTo(0); + } + + @Test + @Order(9) + @DisplayName("Doit retourner un résultat vide pour critères impossibles") + void testEmptyResultForImpossibleCriteria() { + // Given + MembreSearchCriteria criteria = + MembreSearchCriteria.builder().query("membre_inexistant_xyz").build(); + + // When + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); + + // Then + assertThat(result).isNotNull(); + assertThat(result.getTotalElements()).isEqualTo(0); + assertThat(result.getMembres()).isEmpty(); + assertThat(result.isEmpty()).isTrue(); + assertThat(result.getTotalPages()).isEqualTo(0); + } + + @Test + @Order(10) + @DisplayName("Doit valider la cohérence des critères") + void testCriteriaValidation() { + // Given - Critères incohérents + MembreSearchCriteria invalidCriteria = + MembreSearchCriteria.builder() + .ageMin(50) + .ageMax(30) // Âge max < âge min + .build(); + + // When & Then + assertThat(invalidCriteria.isValid()).isFalse(); + } + + @Test + @Order(11) + @DisplayName("Doit avoir des performances acceptables (< 500ms)") + void testSearchPerformance() { + // Given + MembreSearchCriteria criteria = MembreSearchCriteria.builder().includeInactifs(true).build(); + + // When & Then - Mesurer le temps d'exécution + long startTime = System.currentTimeMillis(); + + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 20), Sort.by("nom")); + + long executionTime = System.currentTimeMillis() - startTime; + + // Vérifications + assertThat(result).isNotNull(); + assertThat(executionTime).isLessThan(500L); // Moins de 500ms + + // Log pour monitoring + System.out.printf( + "Recherche avancée exécutée en %d ms pour %d résultats%n", + executionTime, result.getTotalElements()); + } + + @Test + @Order(12) + @DisplayName("Doit gérer les critères avec caractères spéciaux") + void testSearchWithSpecialCharacters() { + // Given + MembreSearchCriteria criteria = + MembreSearchCriteria.builder().query("marie-josé").nom("o'connor").build(); + + // When + MembreSearchResultDTO result = + membreService.searchMembresAdvanced(criteria, Page.of(0, 10), Sort.by("nom")); + + // Then + assertThat(result).isNotNull(); + // La recherche ne doit pas échouer même avec des caractères spéciaux + assertThat(result.getTotalElements()).isGreaterThanOrEqualTo(0); + } +} diff --git a/target/classes/META-INF/beans.xml b/target/classes/META-INF/beans.xml new file mode 100644 index 0000000..1ba4e60 --- /dev/null +++ b/target/classes/META-INF/beans.xml @@ -0,0 +1,8 @@ + + + diff --git a/target/classes/application-minimal.properties b/target/classes/application-minimal.properties new file mode 100644 index 0000000..309e021 --- /dev/null +++ b/target/classes/application-minimal.properties @@ -0,0 +1,56 @@ +# Configuration UnionFlow Server - Mode Minimal +quarkus.application.name=unionflow-server-minimal +quarkus.application.version=1.0.0 + +# Configuration HTTP +quarkus.http.port=8080 +quarkus.http.host=0.0.0.0 + +# Configuration CORS +quarkus.http.cors=true +quarkus.http.cors.origins=* +quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS +quarkus.http.cors.headers=Content-Type,Authorization + +# Configuration Base de données H2 (en mémoire) +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password= +quarkus.datasource.jdbc.url=jdbc:h2:mem:unionflow_minimal;DB_CLOSE_DELAY=-1;MODE=PostgreSQL + +# Configuration Hibernate +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.jdbc.timezone=UTC +quarkus.hibernate-orm.packages=dev.lions.unionflow.server.entity + +# Désactiver Flyway +quarkus.flyway.migrate-at-start=false + +# Désactiver Keycloak temporairement +quarkus.oidc.tenant-enabled=false + +# Chemins publics (tous publics en mode minimal) +quarkus.http.auth.permission.public.paths=/* +quarkus.http.auth.permission.public.policy=permit + +# Configuration OpenAPI +quarkus.smallrye-openapi.info-title=UnionFlow Server API - Minimal +quarkus.smallrye-openapi.info-version=1.0.0 +quarkus.smallrye-openapi.info-description=API REST pour la gestion d'union (mode minimal) +quarkus.smallrye-openapi.servers=http://localhost:8080 + +# Configuration Swagger UI +quarkus.swagger-ui.always-include=true +quarkus.swagger-ui.path=/swagger-ui + +# Configuration santé +quarkus.smallrye-health.root-path=/health + +# Configuration logging +quarkus.log.console.enable=true +quarkus.log.console.level=INFO +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.category."dev.lions.unionflow".level=DEBUG +quarkus.log.category."org.hibernate".level=WARN +quarkus.log.category."io.quarkus".level=INFO diff --git a/target/classes/application-prod.properties b/target/classes/application-prod.properties new file mode 100644 index 0000000..d1dc9c8 --- /dev/null +++ b/target/classes/application-prod.properties @@ -0,0 +1,77 @@ +# Configuration UnionFlow Server - PRODUCTION +# Ce fichier est utilisé avec le profil Quarkus "prod" + +# Configuration HTTP +quarkus.http.port=8085 +quarkus.http.host=0.0.0.0 + +# Configuration CORS - Production (strict) +quarkus.http.cors=true +quarkus.http.cors.origins=${CORS_ORIGINS:https://unionflow.lions.dev,https://security.lions.dev} +quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS +quarkus.http.cors.headers=Content-Type,Authorization +quarkus.http.cors.allow-credentials=true + +# Configuration Base de données PostgreSQL - Production +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=${DB_USERNAME:unionflow} +quarkus.datasource.password=${DB_PASSWORD} +quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://localhost:5432/unionflow} +quarkus.datasource.jdbc.min-size=5 +quarkus.datasource.jdbc.max-size=20 + +# Configuration Hibernate - Production (IMPORTANT: update, pas drop-and-create) +quarkus.hibernate-orm.database.generation=update +quarkus.hibernate-orm.log.sql=false +quarkus.hibernate-orm.jdbc.timezone=UTC +quarkus.hibernate-orm.packages=dev.lions.unionflow.server.entity +quarkus.hibernate-orm.metrics.enabled=false + +# Configuration Flyway - Production (ACTIVÉ) +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=1.0.0 + +# Configuration Keycloak OIDC - Production +quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/unionflow} +quarkus.oidc.client-id=unionflow-server +quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} +quarkus.oidc.tls.verification=required +quarkus.oidc.application-type=service + +# Configuration Keycloak Policy Enforcer +quarkus.keycloak.policy-enforcer.enable=false +quarkus.keycloak.policy-enforcer.lazy-load-paths=true +quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE + +# Chemins publics (non protégés) +quarkus.http.auth.permission.public.paths=/health,/q/*,/favicon.ico +quarkus.http.auth.permission.public.policy=permit + +# Configuration OpenAPI - Production (Swagger désactivé ou protégé) +quarkus.smallrye-openapi.info-title=UnionFlow Server API +quarkus.smallrye-openapi.info-version=1.0.0 +quarkus.smallrye-openapi.info-description=API REST pour la gestion d'union avec authentification Keycloak +quarkus.smallrye-openapi.servers=https://api.lions.dev/unionflow + +# Configuration Swagger UI - Production (DÉSACTIVÉ pour sécurité) +quarkus.swagger-ui.always-include=false + +# Configuration santé +quarkus.smallrye-health.root-path=/health + +# Configuration logging - Production +quarkus.log.console.enable=true +quarkus.log.console.level=INFO +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.category."dev.lions.unionflow".level=INFO +quarkus.log.category."org.hibernate".level=WARN +quarkus.log.category."io.quarkus".level=INFO +quarkus.log.category."org.jboss.resteasy".level=WARN + +# Configuration Wave Money - Production +wave.api.key=${WAVE_API_KEY:} +wave.api.secret=${WAVE_API_SECRET:} +wave.api.base.url=${WAVE_API_BASE_URL:https://api.wave.com/v1} +wave.environment=${WAVE_ENVIRONMENT:production} +wave.webhook.secret=${WAVE_WEBHOOK_SECRET:} diff --git a/target/classes/application-test.properties b/target/classes/application-test.properties new file mode 100644 index 0000000..173d6db --- /dev/null +++ b/target/classes/application-test.properties @@ -0,0 +1,31 @@ +# Configuration UnionFlow Server - Profil Test +# Ce fichier est chargé automatiquement quand le profil 'test' est actif + +# Configuration Base de données H2 pour tests +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=sa +quarkus.datasource.password= +quarkus.datasource.jdbc.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL + +# Configuration Hibernate pour tests +quarkus.hibernate-orm.database.generation=drop-and-create +# Désactiver complètement l'exécution des scripts SQL au démarrage +quarkus.hibernate-orm.sql-load-script-source=none +# Empêcher Hibernate d'exécuter les scripts SQL automatiquement +# Note: Ne pas définir quarkus.hibernate-orm.sql-load-script car une chaîne vide peut causer des problèmes + +# Configuration Flyway pour tests (désactivé complètement) +quarkus.flyway.migrate-at-start=false +quarkus.flyway.enabled=false +quarkus.flyway.baseline-on-migrate=false +# Note: Ne pas définir quarkus.flyway.locations car une chaîne vide cause une erreur de configuration + +# Configuration Keycloak pour tests (désactivé) +quarkus.oidc.tenant-enabled=false +quarkus.keycloak.policy-enforcer.enable=false + +# Configuration HTTP pour tests +quarkus.http.port=0 +quarkus.http.test-port=0 + + diff --git a/target/classes/application.properties b/target/classes/application.properties new file mode 100644 index 0000000..c81a866 --- /dev/null +++ b/target/classes/application.properties @@ -0,0 +1,103 @@ +# Configuration UnionFlow Server +quarkus.application.name=unionflow-server +quarkus.application.version=1.0.0 + +# Configuration HTTP +quarkus.http.port=8085 +quarkus.http.host=0.0.0.0 + +# Configuration CORS +quarkus.http.cors=true +quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:8086,https://unionflow.lions.dev,https://security.lions.dev} +quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS +quarkus.http.cors.headers=Content-Type,Authorization + +# Configuration Base de données PostgreSQL (par défaut) +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=${DB_USERNAME:unionflow} +quarkus.datasource.password=${DB_PASSWORD} +quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://localhost:5432/unionflow} +quarkus.datasource.jdbc.min-size=2 +quarkus.datasource.jdbc.max-size=10 + +# Configuration Base de données PostgreSQL pour développement +%dev.quarkus.datasource.username=skyfile +%dev.quarkus.datasource.password=${DB_PASSWORD_DEV:skyfile} +%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/unionflow + +# Configuration Hibernate +quarkus.hibernate-orm.database.generation=update +quarkus.hibernate-orm.log.sql=false +quarkus.hibernate-orm.jdbc.timezone=UTC +quarkus.hibernate-orm.packages=dev.lions.unionflow.server.entity +# Désactiver l'avertissement PanacheEntity (nous utilisons BaseEntity personnalisé) +quarkus.hibernate-orm.metrics.enabled=false + +# Configuration Hibernate pour développement +%dev.quarkus.hibernate-orm.database.generation=drop-and-create +%dev.quarkus.hibernate-orm.sql-load-script=import.sql +%dev.quarkus.hibernate-orm.log.sql=true + +# Configuration Flyway pour migrations +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=1.0.0 + +# Configuration Flyway pour développement (désactivé) +%dev.quarkus.flyway.migrate-at-start=false + +# Configuration Keycloak OIDC (par défaut) +quarkus.oidc.auth-server-url=http://localhost:8180/realms/unionflow +quarkus.oidc.client-id=unionflow-server +quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} +quarkus.oidc.tls.verification=none +quarkus.oidc.application-type=service + +# Configuration Keycloak pour développement +%dev.quarkus.oidc.tenant-enabled=false +%dev.quarkus.oidc.auth-server-url=http://localhost:8180/realms/unionflow + +# Configuration Keycloak Policy Enforcer (temporairement désactivé) +quarkus.keycloak.policy-enforcer.enable=false +quarkus.keycloak.policy-enforcer.lazy-load-paths=true +quarkus.keycloak.policy-enforcer.enforcement-mode=PERMISSIVE + +# Chemins publics (non protégés) +quarkus.http.auth.permission.public.paths=/health,/q/*,/favicon.ico,/auth/callback,/auth/* +quarkus.http.auth.permission.public.policy=permit + +# Configuration OpenAPI +quarkus.smallrye-openapi.info-title=UnionFlow Server API +quarkus.smallrye-openapi.info-version=1.0.0 +quarkus.smallrye-openapi.info-description=API REST pour la gestion d'union avec authentification Keycloak +quarkus.smallrye-openapi.servers=http://localhost:8085 + +# Configuration Swagger UI +quarkus.swagger-ui.always-include=true +quarkus.swagger-ui.path=/swagger-ui + +# Configuration santé +quarkus.smallrye-health.root-path=/health + +# Configuration logging +quarkus.log.console.enable=true +quarkus.log.console.level=INFO +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.category."dev.lions.unionflow".level=INFO +quarkus.log.category."org.hibernate".level=WARN +quarkus.log.category."io.quarkus".level=INFO + +# Configuration logging pour développement +%dev.quarkus.log.category."dev.lions.unionflow".level=DEBUG +%dev.quarkus.log.category."org.hibernate.SQL".level=DEBUG + +# Configuration Jandex pour résoudre les warnings de réflexion +quarkus.index-dependency.unionflow-server-api.group-id=dev.lions.unionflow +quarkus.index-dependency.unionflow-server-api.artifact-id=unionflow-server-api + +# Configuration Wave Money +wave.api.key=${WAVE_API_KEY:} +wave.api.secret=${WAVE_API_SECRET:} +wave.api.base.url=${WAVE_API_BASE_URL:https://api.wave.com/v1} +wave.environment=${WAVE_ENVIRONMENT:sandbox} +wave.webhook.secret=${WAVE_WEBHOOK_SECRET:} diff --git a/target/classes/db/migration/V1.2__Create_Organisation_Table.sql b/target/classes/db/migration/V1.2__Create_Organisation_Table.sql new file mode 100644 index 0000000..7329794 --- /dev/null +++ b/target/classes/db/migration/V1.2__Create_Organisation_Table.sql @@ -0,0 +1,143 @@ +-- Migration V1.2: Création de la table organisations +-- Auteur: UnionFlow Team +-- Date: 2025-01-15 +-- Description: Création de la table organisations avec toutes les colonnes nécessaires + +-- Création de la table organisations +CREATE TABLE organisations ( + id BIGSERIAL PRIMARY KEY, + + -- Informations de base + nom VARCHAR(200) NOT NULL, + nom_court VARCHAR(50), + type_organisation VARCHAR(50) NOT NULL DEFAULT 'ASSOCIATION', + statut VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', + description TEXT, + date_fondation DATE, + numero_enregistrement VARCHAR(100) UNIQUE, + + -- Informations de contact + email VARCHAR(255) NOT NULL UNIQUE, + telephone VARCHAR(20), + telephone_secondaire VARCHAR(20), + email_secondaire VARCHAR(255), + + -- Adresse + adresse VARCHAR(500), + ville VARCHAR(100), + code_postal VARCHAR(20), + region VARCHAR(100), + pays VARCHAR(100), + + -- Coordonnées géographiques + latitude DECIMAL(9,6) CHECK (latitude >= -90 AND latitude <= 90), + longitude DECIMAL(9,6) CHECK (longitude >= -180 AND longitude <= 180), + + -- Web et réseaux sociaux + site_web VARCHAR(500), + logo VARCHAR(500), + reseaux_sociaux VARCHAR(1000), + + -- Hiérarchie + organisation_parente_id UUID, + niveau_hierarchique INTEGER NOT NULL DEFAULT 0, + + -- Statistiques + nombre_membres INTEGER NOT NULL DEFAULT 0, + nombre_administrateurs INTEGER NOT NULL DEFAULT 0, + + -- Finances + budget_annuel DECIMAL(14,2) CHECK (budget_annuel >= 0), + devise VARCHAR(3) DEFAULT 'XOF', + cotisation_obligatoire BOOLEAN NOT NULL DEFAULT FALSE, + montant_cotisation_annuelle DECIMAL(12,2) CHECK (montant_cotisation_annuelle >= 0), + + -- Informations complémentaires + objectifs TEXT, + activites_principales TEXT, + certifications VARCHAR(500), + partenaires VARCHAR(1000), + notes VARCHAR(1000), + + -- Paramètres + organisation_publique BOOLEAN NOT NULL DEFAULT TRUE, + accepte_nouveaux_membres BOOLEAN NOT NULL DEFAULT TRUE, + + -- Métadonnées + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(100), + modifie_par VARCHAR(100), + version BIGINT NOT NULL DEFAULT 0, + + -- Contraintes + CONSTRAINT chk_organisation_statut CHECK (statut IN ('ACTIVE', 'SUSPENDUE', 'DISSOUTE', 'EN_ATTENTE')), + CONSTRAINT chk_organisation_type CHECK (type_organisation IN ( + 'ASSOCIATION', 'LIONS_CLUB', 'ROTARY_CLUB', 'COOPERATIVE', + 'FONDATION', 'ONG', 'SYNDICAT', 'AUTRE' + )), + CONSTRAINT chk_organisation_devise CHECK (devise IN ('XOF', 'EUR', 'USD', 'GBP', 'CHF')), + CONSTRAINT chk_organisation_niveau CHECK (niveau_hierarchique >= 0 AND niveau_hierarchique <= 10), + CONSTRAINT chk_organisation_membres CHECK (nombre_membres >= 0), + CONSTRAINT chk_organisation_admins CHECK (nombre_administrateurs >= 0) +); + +-- Création des index pour optimiser les performances +CREATE INDEX idx_organisation_nom ON organisations(nom); +CREATE INDEX idx_organisation_email ON organisations(email); +CREATE INDEX idx_organisation_statut ON organisations(statut); +CREATE INDEX idx_organisation_type ON organisations(type_organisation); +CREATE INDEX idx_organisation_ville ON organisations(ville); +CREATE INDEX idx_organisation_pays ON organisations(pays); +CREATE INDEX idx_organisation_parente ON organisations(organisation_parente_id); +CREATE INDEX idx_organisation_numero_enregistrement ON organisations(numero_enregistrement); +CREATE INDEX idx_organisation_actif ON organisations(actif); +CREATE INDEX idx_organisation_date_creation ON organisations(date_creation); +CREATE INDEX idx_organisation_publique ON organisations(organisation_publique); +CREATE INDEX idx_organisation_accepte_membres ON organisations(accepte_nouveaux_membres); + +-- Index composites pour les recherches fréquentes +CREATE INDEX idx_organisation_statut_actif ON organisations(statut, actif); +CREATE INDEX idx_organisation_type_ville ON organisations(type_organisation, ville); +CREATE INDEX idx_organisation_pays_region ON organisations(pays, region); +CREATE INDEX idx_organisation_publique_actif ON organisations(organisation_publique, actif); + +-- Index pour les recherches textuelles +CREATE INDEX idx_organisation_nom_lower ON organisations(LOWER(nom)); +CREATE INDEX idx_organisation_nom_court_lower ON organisations(LOWER(nom_court)); +CREATE INDEX idx_organisation_ville_lower ON organisations(LOWER(ville)); + +-- Ajout de la colonne organisation_id à la table membres (si elle n'existe pas déjà) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'membres' AND column_name = 'organisation_id' + ) THEN + ALTER TABLE membres ADD COLUMN organisation_id BIGINT; + ALTER TABLE membres ADD CONSTRAINT fk_membre_organisation + FOREIGN KEY (organisation_id) REFERENCES organisations(id); + CREATE INDEX idx_membre_organisation ON membres(organisation_id); + END IF; +END $$; + +-- IMPORTANT: Aucune donnée fictive n'est insérée dans ce script de migration. +-- Les données doivent être insérées manuellement via l'interface d'administration +-- ou via des scripts de migration séparés si nécessaire pour la production. + +-- Mise à jour des statistiques de la base de données +ANALYZE organisations; + +-- Commentaires sur la table et les colonnes principales +COMMENT ON TABLE organisations IS 'Table des organisations (Lions Clubs, Associations, Coopératives, etc.)'; +COMMENT ON COLUMN organisations.nom IS 'Nom officiel de l''organisation'; +COMMENT ON COLUMN organisations.nom_court IS 'Nom court ou sigle de l''organisation'; +COMMENT ON COLUMN organisations.type_organisation IS 'Type d''organisation (LIONS_CLUB, ASSOCIATION, etc.)'; +COMMENT ON COLUMN organisations.statut IS 'Statut actuel de l''organisation (ACTIVE, SUSPENDUE, etc.)'; +COMMENT ON COLUMN organisations.organisation_parente_id IS 'ID de l''organisation parente pour la hiérarchie'; +COMMENT ON COLUMN organisations.niveau_hierarchique IS 'Niveau dans la hiérarchie (0 = racine)'; +COMMENT ON COLUMN organisations.nombre_membres IS 'Nombre total de membres actifs'; +COMMENT ON COLUMN organisations.organisation_publique IS 'Si l''organisation est visible publiquement'; +COMMENT ON COLUMN organisations.accepte_nouveaux_membres IS 'Si l''organisation accepte de nouveaux membres'; +COMMENT ON COLUMN organisations.version IS 'Version pour le contrôle de concurrence optimiste'; diff --git a/target/classes/db/migration/V1.3__Convert_Ids_To_UUID.sql b/target/classes/db/migration/V1.3__Convert_Ids_To_UUID.sql new file mode 100644 index 0000000..c921d22 --- /dev/null +++ b/target/classes/db/migration/V1.3__Convert_Ids_To_UUID.sql @@ -0,0 +1,419 @@ +-- Migration V1.3: Conversion des colonnes ID de BIGINT vers UUID +-- Auteur: UnionFlow Team +-- Date: 2025-01-16 +-- Description: Convertit toutes les colonnes ID et clés étrangères de BIGINT vers UUID +-- ATTENTION: Cette migration supprime toutes les données existantes pour simplifier la conversion +-- Pour une migration avec préservation des données, voir V1.3.1__Convert_Ids_To_UUID_With_Data.sql + +-- ============================================ +-- ÉTAPE 1: Suppression des contraintes de clés étrangères +-- ============================================ + +-- Supprimer les contraintes de clés étrangères existantes +DO $$ +BEGIN + -- Supprimer FK membres -> organisations + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'fk_membre_organisation' + AND table_name = 'membres' + ) THEN + ALTER TABLE membres DROP CONSTRAINT fk_membre_organisation; + END IF; + + -- Supprimer FK cotisations -> membres + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name LIKE 'fk_cotisation%' + AND table_name = 'cotisations' + ) THEN + ALTER TABLE cotisations DROP CONSTRAINT IF EXISTS fk_cotisation_membre CASCADE; + END IF; + + -- Supprimer FK evenements -> organisations + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name LIKE 'fk_evenement%' + AND table_name = 'evenements' + ) THEN + ALTER TABLE evenements DROP CONSTRAINT IF EXISTS fk_evenement_organisation CASCADE; + END IF; + + -- Supprimer FK inscriptions_evenement -> membres et evenements + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name LIKE 'fk_inscription%' + AND table_name = 'inscriptions_evenement' + ) THEN + ALTER TABLE inscriptions_evenement DROP CONSTRAINT IF EXISTS fk_inscription_membre CASCADE; + ALTER TABLE inscriptions_evenement DROP CONSTRAINT IF EXISTS fk_inscription_evenement CASCADE; + END IF; + + -- Supprimer FK demandes_aide -> membres et organisations + IF EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name LIKE 'fk_demande%' + AND table_name = 'demandes_aide' + ) THEN + ALTER TABLE demandes_aide DROP CONSTRAINT IF EXISTS fk_demande_demandeur CASCADE; + ALTER TABLE demandes_aide DROP CONSTRAINT IF EXISTS fk_demande_evaluateur CASCADE; + ALTER TABLE demandes_aide DROP CONSTRAINT IF EXISTS fk_demande_organisation CASCADE; + END IF; +END $$; + +-- ============================================ +-- ÉTAPE 2: Supprimer les séquences (BIGSERIAL) +-- ============================================ + +DROP SEQUENCE IF EXISTS membres_SEQ CASCADE; +DROP SEQUENCE IF EXISTS cotisations_SEQ CASCADE; +DROP SEQUENCE IF EXISTS evenements_SEQ CASCADE; +DROP SEQUENCE IF EXISTS organisations_id_seq CASCADE; + +-- ============================================ +-- ÉTAPE 3: Supprimer les tables existantes (pour recréation avec UUID) +-- ============================================ + +-- Supprimer les tables dans l'ordre inverse des dépendances +DROP TABLE IF EXISTS inscriptions_evenement CASCADE; +DROP TABLE IF EXISTS demandes_aide CASCADE; +DROP TABLE IF EXISTS cotisations CASCADE; +DROP TABLE IF EXISTS evenements CASCADE; +DROP TABLE IF EXISTS membres CASCADE; +DROP TABLE IF EXISTS organisations CASCADE; + +-- ============================================ +-- ÉTAPE 4: Recréer les tables avec UUID +-- ============================================ + +-- Table organisations avec UUID +CREATE TABLE organisations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Informations de base + nom VARCHAR(200) NOT NULL, + nom_court VARCHAR(50), + type_organisation VARCHAR(50) NOT NULL DEFAULT 'ASSOCIATION', + statut VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', + description TEXT, + date_fondation DATE, + numero_enregistrement VARCHAR(100) UNIQUE, + + -- Informations de contact + email VARCHAR(255) NOT NULL UNIQUE, + telephone VARCHAR(20), + telephone_secondaire VARCHAR(20), + email_secondaire VARCHAR(255), + + -- Adresse + adresse VARCHAR(500), + ville VARCHAR(100), + code_postal VARCHAR(20), + region VARCHAR(100), + pays VARCHAR(100), + + -- Coordonnées géographiques + latitude DECIMAL(9,6) CHECK (latitude >= -90 AND latitude <= 90), + longitude DECIMAL(9,6) CHECK (longitude >= -180 AND longitude <= 180), + + -- Web et réseaux sociaux + site_web VARCHAR(500), + logo VARCHAR(500), + reseaux_sociaux VARCHAR(1000), + + -- Hiérarchie + organisation_parente_id UUID, + niveau_hierarchique INTEGER NOT NULL DEFAULT 0, + + -- Statistiques + nombre_membres INTEGER NOT NULL DEFAULT 0, + nombre_administrateurs INTEGER NOT NULL DEFAULT 0, + + -- Finances + budget_annuel DECIMAL(14,2) CHECK (budget_annuel >= 0), + devise VARCHAR(3) DEFAULT 'XOF', + cotisation_obligatoire BOOLEAN NOT NULL DEFAULT FALSE, + montant_cotisation_annuelle DECIMAL(12,2) CHECK (montant_cotisation_annuelle >= 0), + + -- Informations complémentaires + objectifs TEXT, + activites_principales TEXT, + certifications VARCHAR(500), + partenaires VARCHAR(1000), + notes VARCHAR(1000), + + -- Paramètres + organisation_publique BOOLEAN NOT NULL DEFAULT TRUE, + accepte_nouveaux_membres BOOLEAN NOT NULL DEFAULT TRUE, + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + -- Contraintes + CONSTRAINT chk_organisation_statut CHECK (statut IN ('ACTIVE', 'SUSPENDUE', 'DISSOUTE', 'EN_ATTENTE')), + CONSTRAINT chk_organisation_type CHECK (type_organisation IN ( + 'ASSOCIATION', 'LIONS_CLUB', 'ROTARY_CLUB', 'COOPERATIVE', + 'FONDATION', 'ONG', 'SYNDICAT', 'AUTRE' + )), + CONSTRAINT chk_organisation_devise CHECK (devise IN ('XOF', 'EUR', 'USD', 'GBP', 'CHF')), + CONSTRAINT chk_organisation_niveau CHECK (niveau_hierarchique >= 0 AND niveau_hierarchique <= 10), + CONSTRAINT chk_organisation_membres CHECK (nombre_membres >= 0), + CONSTRAINT chk_organisation_admins CHECK (nombre_administrateurs >= 0), + + -- Clé étrangère pour hiérarchie + CONSTRAINT fk_organisation_parente FOREIGN KEY (organisation_parente_id) + REFERENCES organisations(id) ON DELETE SET NULL +); + +-- Table membres avec UUID +CREATE TABLE membres ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + numero_membre VARCHAR(20) UNIQUE NOT NULL, + prenom VARCHAR(100) NOT NULL, + nom VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + mot_de_passe VARCHAR(255), + telephone VARCHAR(20), + date_naissance DATE NOT NULL, + date_adhesion DATE NOT NULL, + roles VARCHAR(500), + + -- Clé étrangère vers organisations + organisation_id UUID, + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_membre_organisation FOREIGN KEY (organisation_id) + REFERENCES organisations(id) ON DELETE SET NULL +); + +-- Table cotisations avec UUID +CREATE TABLE cotisations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + numero_reference VARCHAR(50) UNIQUE NOT NULL, + membre_id UUID NOT NULL, + type_cotisation VARCHAR(50) NOT NULL, + montant_du DECIMAL(12,2) NOT NULL CHECK (montant_du >= 0), + montant_paye DECIMAL(12,2) NOT NULL DEFAULT 0 CHECK (montant_paye >= 0), + code_devise VARCHAR(3) NOT NULL DEFAULT 'XOF', + statut VARCHAR(30) NOT NULL, + date_echeance DATE NOT NULL, + date_paiement TIMESTAMP, + description VARCHAR(500), + periode VARCHAR(20), + annee INTEGER NOT NULL CHECK (annee >= 2020 AND annee <= 2100), + mois INTEGER CHECK (mois >= 1 AND mois <= 12), + observations VARCHAR(1000), + recurrente BOOLEAN NOT NULL DEFAULT FALSE, + nombre_rappels INTEGER NOT NULL DEFAULT 0 CHECK (nombre_rappels >= 0), + date_dernier_rappel TIMESTAMP, + valide_par_id UUID, + nom_validateur VARCHAR(100), + date_validation TIMESTAMP, + methode_paiement VARCHAR(50), + reference_paiement VARCHAR(100), + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_cotisation_membre FOREIGN KEY (membre_id) + REFERENCES membres(id) ON DELETE CASCADE, + CONSTRAINT chk_cotisation_statut CHECK (statut IN ('EN_ATTENTE', 'PAYEE', 'EN_RETARD', 'PARTIELLEMENT_PAYEE', 'ANNULEE')), + CONSTRAINT chk_cotisation_devise CHECK (code_devise ~ '^[A-Z]{3}$') +); + +-- Table evenements avec UUID +CREATE TABLE evenements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + titre VARCHAR(200) NOT NULL, + description VARCHAR(2000), + date_debut TIMESTAMP NOT NULL, + date_fin TIMESTAMP, + lieu VARCHAR(255) NOT NULL, + adresse VARCHAR(500), + ville VARCHAR(100), + pays VARCHAR(100), + code_postal VARCHAR(20), + latitude DECIMAL(9,6), + longitude DECIMAL(9,6), + type_evenement VARCHAR(50) NOT NULL, + statut VARCHAR(50) NOT NULL, + url_inscription VARCHAR(500), + url_informations VARCHAR(500), + image_url VARCHAR(500), + capacite_max INTEGER, + cout_participation DECIMAL(12,2), + devise VARCHAR(3), + est_public BOOLEAN NOT NULL DEFAULT TRUE, + tags VARCHAR(500), + notes VARCHAR(1000), + + -- Clé étrangère vers organisations + organisation_id UUID, + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_evenement_organisation FOREIGN KEY (organisation_id) + REFERENCES organisations(id) ON DELETE SET NULL +); + +-- Table inscriptions_evenement avec UUID +CREATE TABLE inscriptions_evenement ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + membre_id UUID NOT NULL, + evenement_id UUID NOT NULL, + date_inscription TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + statut VARCHAR(20) DEFAULT 'CONFIRMEE', + commentaire VARCHAR(500), + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_inscription_membre FOREIGN KEY (membre_id) + REFERENCES membres(id) ON DELETE CASCADE, + CONSTRAINT fk_inscription_evenement FOREIGN KEY (evenement_id) + REFERENCES evenements(id) ON DELETE CASCADE, + CONSTRAINT chk_inscription_statut CHECK (statut IN ('CONFIRMEE', 'EN_ATTENTE', 'ANNULEE', 'REFUSEE')), + CONSTRAINT uk_inscription_membre_evenement UNIQUE (membre_id, evenement_id) +); + +-- Table demandes_aide avec UUID +CREATE TABLE demandes_aide ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + titre VARCHAR(200) NOT NULL, + description TEXT NOT NULL, + type_aide VARCHAR(50) NOT NULL, + statut VARCHAR(50) NOT NULL, + montant_demande DECIMAL(10,2), + montant_approuve DECIMAL(10,2), + date_demande TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_evaluation TIMESTAMP, + date_versement TIMESTAMP, + justification TEXT, + commentaire_evaluation TEXT, + urgence BOOLEAN NOT NULL DEFAULT FALSE, + documents_fournis VARCHAR(500), + + -- Clés étrangères + demandeur_id UUID NOT NULL, + evaluateur_id UUID, + organisation_id UUID NOT NULL, + + -- Métadonnées (héritées de BaseEntity) + actif BOOLEAN NOT NULL DEFAULT TRUE, + date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + date_modification TIMESTAMP, + cree_par VARCHAR(255), + modifie_par VARCHAR(255), + version BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT fk_demande_demandeur FOREIGN KEY (demandeur_id) + REFERENCES membres(id) ON DELETE CASCADE, + CONSTRAINT fk_demande_evaluateur FOREIGN KEY (evaluateur_id) + REFERENCES membres(id) ON DELETE SET NULL, + CONSTRAINT fk_demande_organisation FOREIGN KEY (organisation_id) + REFERENCES organisations(id) ON DELETE CASCADE +); + +-- ============================================ +-- ÉTAPE 5: Recréer les index +-- ============================================ + +-- Index pour organisations +CREATE INDEX idx_organisation_nom ON organisations(nom); +CREATE INDEX idx_organisation_email ON organisations(email); +CREATE INDEX idx_organisation_statut ON organisations(statut); +CREATE INDEX idx_organisation_type ON organisations(type_organisation); +CREATE INDEX idx_organisation_ville ON organisations(ville); +CREATE INDEX idx_organisation_pays ON organisations(pays); +CREATE INDEX idx_organisation_parente ON organisations(organisation_parente_id); +CREATE INDEX idx_organisation_numero_enregistrement ON organisations(numero_enregistrement); +CREATE INDEX idx_organisation_actif ON organisations(actif); +CREATE INDEX idx_organisation_date_creation ON organisations(date_creation); +CREATE INDEX idx_organisation_publique ON organisations(organisation_publique); +CREATE INDEX idx_organisation_accepte_membres ON organisations(accepte_nouveaux_membres); +CREATE INDEX idx_organisation_statut_actif ON organisations(statut, actif); + +-- Index pour membres +CREATE INDEX idx_membre_email ON membres(email); +CREATE INDEX idx_membre_numero ON membres(numero_membre); +CREATE INDEX idx_membre_actif ON membres(actif); +CREATE INDEX idx_membre_organisation ON membres(organisation_id); + +-- Index pour cotisations +CREATE INDEX idx_cotisation_membre ON cotisations(membre_id); +CREATE INDEX idx_cotisation_reference ON cotisations(numero_reference); +CREATE INDEX idx_cotisation_statut ON cotisations(statut); +CREATE INDEX idx_cotisation_echeance ON cotisations(date_echeance); +CREATE INDEX idx_cotisation_type ON cotisations(type_cotisation); +CREATE INDEX idx_cotisation_annee_mois ON cotisations(annee, mois); + +-- Index pour evenements +CREATE INDEX idx_evenement_date_debut ON evenements(date_debut); +CREATE INDEX idx_evenement_statut ON evenements(statut); +CREATE INDEX idx_evenement_type ON evenements(type_evenement); +CREATE INDEX idx_evenement_organisation ON evenements(organisation_id); + +-- Index pour inscriptions_evenement +CREATE INDEX idx_inscription_membre ON inscriptions_evenement(membre_id); +CREATE INDEX idx_inscription_evenement ON inscriptions_evenement(evenement_id); +CREATE INDEX idx_inscription_date ON inscriptions_evenement(date_inscription); + +-- Index pour demandes_aide +CREATE INDEX idx_demande_demandeur ON demandes_aide(demandeur_id); +CREATE INDEX idx_demande_evaluateur ON demandes_aide(evaluateur_id); +CREATE INDEX idx_demande_organisation ON demandes_aide(organisation_id); +CREATE INDEX idx_demande_statut ON demandes_aide(statut); +CREATE INDEX idx_demande_type ON demandes_aide(type_aide); +CREATE INDEX idx_demande_date_demande ON demandes_aide(date_demande); + +-- ============================================ +-- ÉTAPE 6: Commentaires sur les tables +-- ============================================ + +COMMENT ON TABLE organisations IS 'Table des organisations (Lions Clubs, Associations, Coopératives, etc.) avec UUID'; +COMMENT ON TABLE membres IS 'Table des membres avec UUID'; +COMMENT ON TABLE cotisations IS 'Table des cotisations avec UUID'; +COMMENT ON TABLE evenements IS 'Table des événements avec UUID'; +COMMENT ON TABLE inscriptions_evenement IS 'Table des inscriptions aux événements avec UUID'; +COMMENT ON TABLE demandes_aide IS 'Table des demandes d''aide avec UUID'; + +COMMENT ON COLUMN organisations.id IS 'UUID unique de l''organisation'; +COMMENT ON COLUMN membres.id IS 'UUID unique du membre'; +COMMENT ON COLUMN cotisations.id IS 'UUID unique de la cotisation'; +COMMENT ON COLUMN evenements.id IS 'UUID unique de l''événement'; +COMMENT ON COLUMN inscriptions_evenement.id IS 'UUID unique de l''inscription'; +COMMENT ON COLUMN demandes_aide.id IS 'UUID unique de la demande d''aide'; + diff --git a/target/classes/de/lions/unionflow/server/auth/AuthCallbackResource.class b/target/classes/de/lions/unionflow/server/auth/AuthCallbackResource.class new file mode 100644 index 0000000..1e4a406 Binary files /dev/null and b/target/classes/de/lions/unionflow/server/auth/AuthCallbackResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/UnionFlowServerApplication.class b/target/classes/dev/lions/unionflow/server/UnionFlowServerApplication.class new file mode 100644 index 0000000..9f1e399 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/UnionFlowServerApplication.class differ diff --git a/target/classes/dev/lions/unionflow/server/dto/EvenementMobileDTO$EvenementMobileDTOBuilder.class b/target/classes/dev/lions/unionflow/server/dto/EvenementMobileDTO$EvenementMobileDTOBuilder.class new file mode 100644 index 0000000..69f58f9 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/dto/EvenementMobileDTO$EvenementMobileDTOBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/dto/EvenementMobileDTO.class b/target/classes/dev/lions/unionflow/server/dto/EvenementMobileDTO.class new file mode 100644 index 0000000..c302350 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/dto/EvenementMobileDTO.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Adhesion$AdhesionBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Adhesion$AdhesionBuilder.class new file mode 100644 index 0000000..123bfbf Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Adhesion$AdhesionBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Adhesion.class b/target/classes/dev/lions/unionflow/server/entity/Adhesion.class new file mode 100644 index 0000000..82bb188 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Adhesion.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Adresse$AdresseBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Adresse$AdresseBuilder.class new file mode 100644 index 0000000..52b08d1 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Adresse$AdresseBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Adresse.class b/target/classes/dev/lions/unionflow/server/entity/Adresse.class new file mode 100644 index 0000000..d12704a Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Adresse.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/AuditLog.class b/target/classes/dev/lions/unionflow/server/entity/AuditLog.class new file mode 100644 index 0000000..17aadba Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/AuditLog.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/BaseEntity.class b/target/classes/dev/lions/unionflow/server/entity/BaseEntity.class new file mode 100644 index 0000000..248c535 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/BaseEntity.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/CompteComptable$CompteComptableBuilder.class b/target/classes/dev/lions/unionflow/server/entity/CompteComptable$CompteComptableBuilder.class new file mode 100644 index 0000000..1efd80c Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/CompteComptable$CompteComptableBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/CompteComptable.class b/target/classes/dev/lions/unionflow/server/entity/CompteComptable.class new file mode 100644 index 0000000..75afa8b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/CompteComptable.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/CompteWave$CompteWaveBuilder.class b/target/classes/dev/lions/unionflow/server/entity/CompteWave$CompteWaveBuilder.class new file mode 100644 index 0000000..6f6c365 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/CompteWave$CompteWaveBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/CompteWave.class b/target/classes/dev/lions/unionflow/server/entity/CompteWave.class new file mode 100644 index 0000000..becd906 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/CompteWave.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/ConfigurationWave$ConfigurationWaveBuilder.class b/target/classes/dev/lions/unionflow/server/entity/ConfigurationWave$ConfigurationWaveBuilder.class new file mode 100644 index 0000000..02a337a Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/ConfigurationWave$ConfigurationWaveBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/ConfigurationWave.class b/target/classes/dev/lions/unionflow/server/entity/ConfigurationWave.class new file mode 100644 index 0000000..0d8542a Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/ConfigurationWave.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Cotisation$CotisationBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Cotisation$CotisationBuilder.class new file mode 100644 index 0000000..98617e6 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Cotisation$CotisationBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Cotisation.class b/target/classes/dev/lions/unionflow/server/entity/Cotisation.class new file mode 100644 index 0000000..4bb3945 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Cotisation.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/DemandeAide$DemandeAideBuilder.class b/target/classes/dev/lions/unionflow/server/entity/DemandeAide$DemandeAideBuilder.class new file mode 100644 index 0000000..d681058 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/DemandeAide$DemandeAideBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/DemandeAide.class b/target/classes/dev/lions/unionflow/server/entity/DemandeAide.class new file mode 100644 index 0000000..9c11590 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/DemandeAide.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Document$DocumentBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Document$DocumentBuilder.class new file mode 100644 index 0000000..8adaf1c Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Document$DocumentBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Document.class b/target/classes/dev/lions/unionflow/server/entity/Document.class new file mode 100644 index 0000000..bcb4b67 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Document.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/EcritureComptable$EcritureComptableBuilder.class b/target/classes/dev/lions/unionflow/server/entity/EcritureComptable$EcritureComptableBuilder.class new file mode 100644 index 0000000..9f15da8 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/EcritureComptable$EcritureComptableBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/EcritureComptable.class b/target/classes/dev/lions/unionflow/server/entity/EcritureComptable.class new file mode 100644 index 0000000..81bd4bb Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/EcritureComptable.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Evenement$EvenementBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Evenement$EvenementBuilder.class new file mode 100644 index 0000000..a7efd13 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Evenement$EvenementBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Evenement$StatutEvenement.class b/target/classes/dev/lions/unionflow/server/entity/Evenement$StatutEvenement.class new file mode 100644 index 0000000..7a8f79b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Evenement$StatutEvenement.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Evenement$TypeEvenement.class b/target/classes/dev/lions/unionflow/server/entity/Evenement$TypeEvenement.class new file mode 100644 index 0000000..6a987bd Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Evenement$TypeEvenement.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Evenement.class b/target/classes/dev/lions/unionflow/server/entity/Evenement.class new file mode 100644 index 0000000..117316b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Evenement.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/InscriptionEvenement$InscriptionEvenementBuilder.class b/target/classes/dev/lions/unionflow/server/entity/InscriptionEvenement$InscriptionEvenementBuilder.class new file mode 100644 index 0000000..bf12502 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/InscriptionEvenement$InscriptionEvenementBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/InscriptionEvenement$StatutInscription.class b/target/classes/dev/lions/unionflow/server/entity/InscriptionEvenement$StatutInscription.class new file mode 100644 index 0000000..b6eca0b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/InscriptionEvenement$StatutInscription.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/InscriptionEvenement.class b/target/classes/dev/lions/unionflow/server/entity/InscriptionEvenement.class new file mode 100644 index 0000000..9897ef9 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/InscriptionEvenement.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/JournalComptable$JournalComptableBuilder.class b/target/classes/dev/lions/unionflow/server/entity/JournalComptable$JournalComptableBuilder.class new file mode 100644 index 0000000..ada87da Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/JournalComptable$JournalComptableBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/JournalComptable.class b/target/classes/dev/lions/unionflow/server/entity/JournalComptable.class new file mode 100644 index 0000000..ace7795 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/JournalComptable.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/LigneEcriture$LigneEcritureBuilder.class b/target/classes/dev/lions/unionflow/server/entity/LigneEcriture$LigneEcritureBuilder.class new file mode 100644 index 0000000..10dc36e Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/LigneEcriture$LigneEcritureBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/LigneEcriture.class b/target/classes/dev/lions/unionflow/server/entity/LigneEcriture.class new file mode 100644 index 0000000..da95e15 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/LigneEcriture.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Membre$MembreBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Membre$MembreBuilder.class new file mode 100644 index 0000000..e9b2721 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Membre$MembreBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Membre.class b/target/classes/dev/lions/unionflow/server/entity/Membre.class new file mode 100644 index 0000000..ee7aa6e Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Membre.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/MembreRole$MembreRoleBuilder.class b/target/classes/dev/lions/unionflow/server/entity/MembreRole$MembreRoleBuilder.class new file mode 100644 index 0000000..91ee4a9 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/MembreRole$MembreRoleBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/MembreRole.class b/target/classes/dev/lions/unionflow/server/entity/MembreRole.class new file mode 100644 index 0000000..4dc286c Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/MembreRole.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Notification$NotificationBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Notification$NotificationBuilder.class new file mode 100644 index 0000000..edaae40 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Notification$NotificationBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Notification.class b/target/classes/dev/lions/unionflow/server/entity/Notification.class new file mode 100644 index 0000000..d80fb74 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Notification.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Organisation$OrganisationBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Organisation$OrganisationBuilder.class new file mode 100644 index 0000000..9078ff8 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Organisation$OrganisationBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Organisation.class b/target/classes/dev/lions/unionflow/server/entity/Organisation.class new file mode 100644 index 0000000..13802c6 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Organisation.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Paiement$PaiementBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Paiement$PaiementBuilder.class new file mode 100644 index 0000000..185db26 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Paiement$PaiementBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Paiement.class b/target/classes/dev/lions/unionflow/server/entity/Paiement.class new file mode 100644 index 0000000..25b1b8d Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Paiement.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PaiementAdhesion$PaiementAdhesionBuilder.class b/target/classes/dev/lions/unionflow/server/entity/PaiementAdhesion$PaiementAdhesionBuilder.class new file mode 100644 index 0000000..4b79142 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PaiementAdhesion$PaiementAdhesionBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PaiementAdhesion.class b/target/classes/dev/lions/unionflow/server/entity/PaiementAdhesion.class new file mode 100644 index 0000000..92c3a31 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PaiementAdhesion.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PaiementAide$PaiementAideBuilder.class b/target/classes/dev/lions/unionflow/server/entity/PaiementAide$PaiementAideBuilder.class new file mode 100644 index 0000000..7c3547f Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PaiementAide$PaiementAideBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PaiementAide.class b/target/classes/dev/lions/unionflow/server/entity/PaiementAide.class new file mode 100644 index 0000000..aa5bd10 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PaiementAide.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PaiementCotisation$PaiementCotisationBuilder.class b/target/classes/dev/lions/unionflow/server/entity/PaiementCotisation$PaiementCotisationBuilder.class new file mode 100644 index 0000000..ee582c4 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PaiementCotisation$PaiementCotisationBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PaiementCotisation.class b/target/classes/dev/lions/unionflow/server/entity/PaiementCotisation.class new file mode 100644 index 0000000..ab3c820 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PaiementCotisation.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PaiementEvenement$PaiementEvenementBuilder.class b/target/classes/dev/lions/unionflow/server/entity/PaiementEvenement$PaiementEvenementBuilder.class new file mode 100644 index 0000000..3548795 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PaiementEvenement$PaiementEvenementBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PaiementEvenement.class b/target/classes/dev/lions/unionflow/server/entity/PaiementEvenement.class new file mode 100644 index 0000000..a3bf404 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PaiementEvenement.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Permission$PermissionBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Permission$PermissionBuilder.class new file mode 100644 index 0000000..8a589a3 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Permission$PermissionBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Permission.class b/target/classes/dev/lions/unionflow/server/entity/Permission.class new file mode 100644 index 0000000..4406f67 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Permission.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PieceJointe$PieceJointeBuilder.class b/target/classes/dev/lions/unionflow/server/entity/PieceJointe$PieceJointeBuilder.class new file mode 100644 index 0000000..c15ee90 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PieceJointe$PieceJointeBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/PieceJointe.class b/target/classes/dev/lions/unionflow/server/entity/PieceJointe.class new file mode 100644 index 0000000..fccb5e9 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/PieceJointe.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Role$RoleBuilder.class b/target/classes/dev/lions/unionflow/server/entity/Role$RoleBuilder.class new file mode 100644 index 0000000..abc23a0 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Role$RoleBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Role$TypeRole.class b/target/classes/dev/lions/unionflow/server/entity/Role$TypeRole.class new file mode 100644 index 0000000..bc8b01b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Role$TypeRole.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/Role.class b/target/classes/dev/lions/unionflow/server/entity/Role.class new file mode 100644 index 0000000..8f28c44 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/Role.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/RolePermission$RolePermissionBuilder.class b/target/classes/dev/lions/unionflow/server/entity/RolePermission$RolePermissionBuilder.class new file mode 100644 index 0000000..a77a6c8 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/RolePermission$RolePermissionBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/RolePermission.class b/target/classes/dev/lions/unionflow/server/entity/RolePermission.class new file mode 100644 index 0000000..a07c9d7 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/RolePermission.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/TemplateNotification$TemplateNotificationBuilder.class b/target/classes/dev/lions/unionflow/server/entity/TemplateNotification$TemplateNotificationBuilder.class new file mode 100644 index 0000000..0f437f5 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/TemplateNotification$TemplateNotificationBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/TemplateNotification.class b/target/classes/dev/lions/unionflow/server/entity/TemplateNotification.class new file mode 100644 index 0000000..934b3c1 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/TemplateNotification.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/TransactionWave$TransactionWaveBuilder.class b/target/classes/dev/lions/unionflow/server/entity/TransactionWave$TransactionWaveBuilder.class new file mode 100644 index 0000000..9ec8033 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/TransactionWave$TransactionWaveBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/TransactionWave.class b/target/classes/dev/lions/unionflow/server/entity/TransactionWave.class new file mode 100644 index 0000000..84634f2 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/TransactionWave.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/TypeOrganisationEntity.class b/target/classes/dev/lions/unionflow/server/entity/TypeOrganisationEntity.class new file mode 100644 index 0000000..8228ae8 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/TypeOrganisationEntity.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/WebhookWave$WebhookWaveBuilder.class b/target/classes/dev/lions/unionflow/server/entity/WebhookWave$WebhookWaveBuilder.class new file mode 100644 index 0000000..5ef538a Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/WebhookWave$WebhookWaveBuilder.class differ diff --git a/target/classes/dev/lions/unionflow/server/entity/WebhookWave.class b/target/classes/dev/lions/unionflow/server/entity/WebhookWave.class new file mode 100644 index 0000000..1e33898 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/entity/WebhookWave.class differ diff --git a/target/classes/dev/lions/unionflow/server/exception/JsonProcessingExceptionMapper.class b/target/classes/dev/lions/unionflow/server/exception/JsonProcessingExceptionMapper.class new file mode 100644 index 0000000..be8002d Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/exception/JsonProcessingExceptionMapper.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/AdhesionRepository.class b/target/classes/dev/lions/unionflow/server/repository/AdhesionRepository.class new file mode 100644 index 0000000..9c757b8 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/AdhesionRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/AdresseRepository.class b/target/classes/dev/lions/unionflow/server/repository/AdresseRepository.class new file mode 100644 index 0000000..a4a76c3 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/AdresseRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/AuditLogRepository.class b/target/classes/dev/lions/unionflow/server/repository/AuditLogRepository.class new file mode 100644 index 0000000..61a69b3 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/AuditLogRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/BaseRepository.class b/target/classes/dev/lions/unionflow/server/repository/BaseRepository.class new file mode 100644 index 0000000..ba718ba Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/BaseRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/CompteComptableRepository.class b/target/classes/dev/lions/unionflow/server/repository/CompteComptableRepository.class new file mode 100644 index 0000000..ed88b90 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/CompteComptableRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/CompteWaveRepository.class b/target/classes/dev/lions/unionflow/server/repository/CompteWaveRepository.class new file mode 100644 index 0000000..72fbdbe Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/CompteWaveRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/ConfigurationWaveRepository.class b/target/classes/dev/lions/unionflow/server/repository/ConfigurationWaveRepository.class new file mode 100644 index 0000000..cbd1710 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/ConfigurationWaveRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/CotisationRepository.class b/target/classes/dev/lions/unionflow/server/repository/CotisationRepository.class new file mode 100644 index 0000000..0a52216 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/CotisationRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/DemandeAideRepository.class b/target/classes/dev/lions/unionflow/server/repository/DemandeAideRepository.class new file mode 100644 index 0000000..753c1bc Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/DemandeAideRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/DocumentRepository.class b/target/classes/dev/lions/unionflow/server/repository/DocumentRepository.class new file mode 100644 index 0000000..8f13a00 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/DocumentRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/EcritureComptableRepository.class b/target/classes/dev/lions/unionflow/server/repository/EcritureComptableRepository.class new file mode 100644 index 0000000..6a38356 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/EcritureComptableRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/EvenementRepository.class b/target/classes/dev/lions/unionflow/server/repository/EvenementRepository.class new file mode 100644 index 0000000..8e8028b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/EvenementRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/JournalComptableRepository.class b/target/classes/dev/lions/unionflow/server/repository/JournalComptableRepository.class new file mode 100644 index 0000000..47bb0bd Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/JournalComptableRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/LigneEcritureRepository.class b/target/classes/dev/lions/unionflow/server/repository/LigneEcritureRepository.class new file mode 100644 index 0000000..acffeed Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/LigneEcritureRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/MembreRepository.class b/target/classes/dev/lions/unionflow/server/repository/MembreRepository.class new file mode 100644 index 0000000..dd74524 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/MembreRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/MembreRoleRepository.class b/target/classes/dev/lions/unionflow/server/repository/MembreRoleRepository.class new file mode 100644 index 0000000..444e0af Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/MembreRoleRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/NotificationRepository.class b/target/classes/dev/lions/unionflow/server/repository/NotificationRepository.class new file mode 100644 index 0000000..b88955f Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/NotificationRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/OrganisationRepository.class b/target/classes/dev/lions/unionflow/server/repository/OrganisationRepository.class new file mode 100644 index 0000000..aecd772 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/OrganisationRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/PaiementRepository.class b/target/classes/dev/lions/unionflow/server/repository/PaiementRepository.class new file mode 100644 index 0000000..9557d7a Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/PaiementRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/PermissionRepository.class b/target/classes/dev/lions/unionflow/server/repository/PermissionRepository.class new file mode 100644 index 0000000..0f2a059 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/PermissionRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/PieceJointeRepository.class b/target/classes/dev/lions/unionflow/server/repository/PieceJointeRepository.class new file mode 100644 index 0000000..1d8ae59 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/PieceJointeRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/RolePermissionRepository.class b/target/classes/dev/lions/unionflow/server/repository/RolePermissionRepository.class new file mode 100644 index 0000000..94f522e Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/RolePermissionRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/RoleRepository.class b/target/classes/dev/lions/unionflow/server/repository/RoleRepository.class new file mode 100644 index 0000000..7a24851 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/RoleRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/TemplateNotificationRepository.class b/target/classes/dev/lions/unionflow/server/repository/TemplateNotificationRepository.class new file mode 100644 index 0000000..576a631 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/TemplateNotificationRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/TransactionWaveRepository.class b/target/classes/dev/lions/unionflow/server/repository/TransactionWaveRepository.class new file mode 100644 index 0000000..396beaf Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/TransactionWaveRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/TypeOrganisationRepository.class b/target/classes/dev/lions/unionflow/server/repository/TypeOrganisationRepository.class new file mode 100644 index 0000000..5ca086d Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/TypeOrganisationRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/repository/WebhookWaveRepository.class b/target/classes/dev/lions/unionflow/server/repository/WebhookWaveRepository.class new file mode 100644 index 0000000..48986ab Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/repository/WebhookWaveRepository.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/AdhesionResource.class b/target/classes/dev/lions/unionflow/server/resource/AdhesionResource.class new file mode 100644 index 0000000..b6bd25c Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/AdhesionResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/AnalyticsResource.class b/target/classes/dev/lions/unionflow/server/resource/AnalyticsResource.class new file mode 100644 index 0000000..9b86ab7 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/AnalyticsResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/AuditResource.class b/target/classes/dev/lions/unionflow/server/resource/AuditResource.class new file mode 100644 index 0000000..7b13119 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/AuditResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/ComptabiliteResource$ErrorResponse.class b/target/classes/dev/lions/unionflow/server/resource/ComptabiliteResource$ErrorResponse.class new file mode 100644 index 0000000..2a02f1c Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/ComptabiliteResource$ErrorResponse.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/ComptabiliteResource.class b/target/classes/dev/lions/unionflow/server/resource/ComptabiliteResource.class new file mode 100644 index 0000000..a17bd92 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/ComptabiliteResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/CotisationResource.class b/target/classes/dev/lions/unionflow/server/resource/CotisationResource.class new file mode 100644 index 0000000..6ad70db Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/CotisationResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/DashboardResource.class b/target/classes/dev/lions/unionflow/server/resource/DashboardResource.class new file mode 100644 index 0000000..b48e90d Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/DashboardResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/DocumentResource$ErrorResponse.class b/target/classes/dev/lions/unionflow/server/resource/DocumentResource$ErrorResponse.class new file mode 100644 index 0000000..d4d66c9 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/DocumentResource$ErrorResponse.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/DocumentResource.class b/target/classes/dev/lions/unionflow/server/resource/DocumentResource.class new file mode 100644 index 0000000..371756d Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/DocumentResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/EvenementResource.class b/target/classes/dev/lions/unionflow/server/resource/EvenementResource.class new file mode 100644 index 0000000..9b1bed7 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/EvenementResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/ExportResource.class b/target/classes/dev/lions/unionflow/server/resource/ExportResource.class new file mode 100644 index 0000000..e0e3fc3 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/ExportResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/HealthResource.class b/target/classes/dev/lions/unionflow/server/resource/HealthResource.class new file mode 100644 index 0000000..3ac090e Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/HealthResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/MembreResource.class b/target/classes/dev/lions/unionflow/server/resource/MembreResource.class new file mode 100644 index 0000000..a0520c8 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/MembreResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/NotificationResource$ErrorResponse.class b/target/classes/dev/lions/unionflow/server/resource/NotificationResource$ErrorResponse.class new file mode 100644 index 0000000..53eab4d Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/NotificationResource$ErrorResponse.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/NotificationResource$NotificationGroupeeRequest.class b/target/classes/dev/lions/unionflow/server/resource/NotificationResource$NotificationGroupeeRequest.class new file mode 100644 index 0000000..7230ea6 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/NotificationResource$NotificationGroupeeRequest.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/NotificationResource.class b/target/classes/dev/lions/unionflow/server/resource/NotificationResource.class new file mode 100644 index 0000000..6b5aaab Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/NotificationResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/OrganisationResource.class b/target/classes/dev/lions/unionflow/server/resource/OrganisationResource.class new file mode 100644 index 0000000..fe7e9f8 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/OrganisationResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/PaiementResource$ErrorResponse.class b/target/classes/dev/lions/unionflow/server/resource/PaiementResource$ErrorResponse.class new file mode 100644 index 0000000..3fcd47f Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/PaiementResource$ErrorResponse.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/PaiementResource.class b/target/classes/dev/lions/unionflow/server/resource/PaiementResource.class new file mode 100644 index 0000000..76f4181 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/PaiementResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/PreferencesResource.class b/target/classes/dev/lions/unionflow/server/resource/PreferencesResource.class new file mode 100644 index 0000000..53203f5 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/PreferencesResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/TypeOrganisationResource.class b/target/classes/dev/lions/unionflow/server/resource/TypeOrganisationResource.class new file mode 100644 index 0000000..47cbbaa Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/TypeOrganisationResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/WaveResource$ErrorResponse.class b/target/classes/dev/lions/unionflow/server/resource/WaveResource$ErrorResponse.class new file mode 100644 index 0000000..64f1d05 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/WaveResource$ErrorResponse.class differ diff --git a/target/classes/dev/lions/unionflow/server/resource/WaveResource.class b/target/classes/dev/lions/unionflow/server/resource/WaveResource.class new file mode 100644 index 0000000..26557b6 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/resource/WaveResource.class differ diff --git a/target/classes/dev/lions/unionflow/server/security/SecurityConfig$Permissions.class b/target/classes/dev/lions/unionflow/server/security/SecurityConfig$Permissions.class new file mode 100644 index 0000000..548049a Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/security/SecurityConfig$Permissions.class differ diff --git a/target/classes/dev/lions/unionflow/server/security/SecurityConfig$Roles.class b/target/classes/dev/lions/unionflow/server/security/SecurityConfig$Roles.class new file mode 100644 index 0000000..c785344 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/security/SecurityConfig$Roles.class differ diff --git a/target/classes/dev/lions/unionflow/server/security/SecurityConfig.class b/target/classes/dev/lions/unionflow/server/security/SecurityConfig.class new file mode 100644 index 0000000..478b37a Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/security/SecurityConfig.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/AdhesionService.class b/target/classes/dev/lions/unionflow/server/service/AdhesionService.class new file mode 100644 index 0000000..7f52c7b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/AdhesionService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/AdresseService.class b/target/classes/dev/lions/unionflow/server/service/AdresseService.class new file mode 100644 index 0000000..2cb338c Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/AdresseService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/AnalyticsService.class b/target/classes/dev/lions/unionflow/server/service/AnalyticsService.class new file mode 100644 index 0000000..7278b1e Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/AnalyticsService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/AuditService.class b/target/classes/dev/lions/unionflow/server/service/AuditService.class new file mode 100644 index 0000000..3b42cfb Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/AuditService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/ComptabiliteService.class b/target/classes/dev/lions/unionflow/server/service/ComptabiliteService.class new file mode 100644 index 0000000..5080b57 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/ComptabiliteService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/CotisationService.class b/target/classes/dev/lions/unionflow/server/service/CotisationService.class new file mode 100644 index 0000000..4466e94 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/CotisationService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/DashboardServiceImpl.class b/target/classes/dev/lions/unionflow/server/service/DashboardServiceImpl.class new file mode 100644 index 0000000..10dc048 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/DashboardServiceImpl.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/DemandeAideService.class b/target/classes/dev/lions/unionflow/server/service/DemandeAideService.class new file mode 100644 index 0000000..bb7bb7d Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/DemandeAideService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/DocumentService.class b/target/classes/dev/lions/unionflow/server/service/DocumentService.class new file mode 100644 index 0000000..8dfc79d Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/DocumentService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/EvenementService.class b/target/classes/dev/lions/unionflow/server/service/EvenementService.class new file mode 100644 index 0000000..f19d816 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/EvenementService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/ExportService.class b/target/classes/dev/lions/unionflow/server/service/ExportService.class new file mode 100644 index 0000000..625dde7 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/ExportService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/KPICalculatorService.class b/target/classes/dev/lions/unionflow/server/service/KPICalculatorService.class new file mode 100644 index 0000000..4552c5b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/KPICalculatorService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/KeycloakService.class b/target/classes/dev/lions/unionflow/server/service/KeycloakService.class new file mode 100644 index 0000000..2e87a54 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/KeycloakService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/MatchingService$ResultatMatching.class b/target/classes/dev/lions/unionflow/server/service/MatchingService$ResultatMatching.class new file mode 100644 index 0000000..9f7bdf1 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/MatchingService$ResultatMatching.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/MatchingService.class b/target/classes/dev/lions/unionflow/server/service/MatchingService.class new file mode 100644 index 0000000..596c53b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/MatchingService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/MembreImportExportService$ResultatImport.class b/target/classes/dev/lions/unionflow/server/service/MembreImportExportService$ResultatImport.class new file mode 100644 index 0000000..fc27610 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/MembreImportExportService$ResultatImport.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/MembreImportExportService.class b/target/classes/dev/lions/unionflow/server/service/MembreImportExportService.class new file mode 100644 index 0000000..bcb4d51 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/MembreImportExportService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/MembreService.class b/target/classes/dev/lions/unionflow/server/service/MembreService.class new file mode 100644 index 0000000..0f993fa Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/MembreService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/NotificationHistoryService$NotificationHistoryEntry$Builder.class b/target/classes/dev/lions/unionflow/server/service/NotificationHistoryService$NotificationHistoryEntry$Builder.class new file mode 100644 index 0000000..5730985 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/NotificationHistoryService$NotificationHistoryEntry$Builder.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/NotificationHistoryService$NotificationHistoryEntry.class b/target/classes/dev/lions/unionflow/server/service/NotificationHistoryService$NotificationHistoryEntry.class new file mode 100644 index 0000000..19db07f Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/NotificationHistoryService$NotificationHistoryEntry.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/NotificationHistoryService.class b/target/classes/dev/lions/unionflow/server/service/NotificationHistoryService.class new file mode 100644 index 0000000..6f07454 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/NotificationHistoryService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/NotificationService.class b/target/classes/dev/lions/unionflow/server/service/NotificationService.class new file mode 100644 index 0000000..d2995f4 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/NotificationService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/OrganisationService.class b/target/classes/dev/lions/unionflow/server/service/OrganisationService.class new file mode 100644 index 0000000..9868e8c Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/OrganisationService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/PaiementService.class b/target/classes/dev/lions/unionflow/server/service/PaiementService.class new file mode 100644 index 0000000..07d1b40 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/PaiementService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/PermissionService.class b/target/classes/dev/lions/unionflow/server/service/PermissionService.class new file mode 100644 index 0000000..ac4c238 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/PermissionService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/PreferencesNotificationService.class b/target/classes/dev/lions/unionflow/server/service/PreferencesNotificationService.class new file mode 100644 index 0000000..1fe9b2f Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/PreferencesNotificationService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/PropositionAideService.class b/target/classes/dev/lions/unionflow/server/service/PropositionAideService.class new file mode 100644 index 0000000..d533071 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/PropositionAideService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/RoleService.class b/target/classes/dev/lions/unionflow/server/service/RoleService.class new file mode 100644 index 0000000..17391fd Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/RoleService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/TrendAnalysisService$StatistiquesDTO.class b/target/classes/dev/lions/unionflow/server/service/TrendAnalysisService$StatistiquesDTO.class new file mode 100644 index 0000000..90d6cca Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/TrendAnalysisService$StatistiquesDTO.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/TrendAnalysisService$TendanceDTO.class b/target/classes/dev/lions/unionflow/server/service/TrendAnalysisService$TendanceDTO.class new file mode 100644 index 0000000..2a56785 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/TrendAnalysisService$TendanceDTO.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/TrendAnalysisService.class b/target/classes/dev/lions/unionflow/server/service/TrendAnalysisService.class new file mode 100644 index 0000000..af8f55a Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/TrendAnalysisService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/TypeOrganisationService.class b/target/classes/dev/lions/unionflow/server/service/TypeOrganisationService.class new file mode 100644 index 0000000..0b9f17d Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/TypeOrganisationService.class differ diff --git a/target/classes/dev/lions/unionflow/server/service/WaveService.class b/target/classes/dev/lions/unionflow/server/service/WaveService.class new file mode 100644 index 0000000..05f3555 Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/service/WaveService.class differ diff --git a/target/classes/dev/lions/unionflow/server/util/IdConverter.class b/target/classes/dev/lions/unionflow/server/util/IdConverter.class new file mode 100644 index 0000000..6c68d2b Binary files /dev/null and b/target/classes/dev/lions/unionflow/server/util/IdConverter.class differ diff --git a/target/classes/import.sql b/target/classes/import.sql new file mode 100644 index 0000000..8b3c0d8 --- /dev/null +++ b/target/classes/import.sql @@ -0,0 +1,10 @@ +-- Script d'insertion de données initiales pour UnionFlow +-- Ce fichier est exécuté automatiquement par Hibernate au démarrage +-- Utilisé uniquement en mode développement (quarkus.hibernate-orm.database.generation=drop-and-create) +-- +-- IMPORTANT: Ce fichier ne doit PAS contenir de données fictives pour la production. +-- Les données doivent être insérées manuellement via l'interface d'administration +-- ou via des scripts de migration Flyway si nécessaire. +-- +-- Ce fichier est laissé vide intentionnellement pour éviter l'insertion automatique +-- de données fictives lors du démarrage du serveur. diff --git a/target/classes/keycloak/unionflow-realm.json b/target/classes/keycloak/unionflow-realm.json new file mode 100644 index 0000000..218ff11 --- /dev/null +++ b/target/classes/keycloak/unionflow-realm.json @@ -0,0 +1,307 @@ +{ + "realm": "unionflow", + "displayName": "UnionFlow", + "displayNameHtml": "

UnionFlow
", + "enabled": true, + "sslRequired": "external", + "registrationAllowed": true, + "registrationEmailAsUsername": true, + "rememberMe": true, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": false, + "bruteForceProtected": true, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "defaultRoles": ["offline_access", "uma_authorization", "default-roles-unionflow"], + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "supportedLocales": ["fr", "en"], + "defaultLocale": "fr", + "internationalizationEnabled": true, + "clients": [ + { + "clientId": "unionflow-server", + "name": "UnionFlow Server API", + "description": "Client pour l'API serveur UnionFlow", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "dev-secret", + "redirectUris": ["http://localhost:8080/*"], + "webOrigins": ["http://localhost:8080", "http://localhost:3000"], + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "name": "given_name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "name": "family_name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "name": "roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ], + "defaultClientScopes": ["web-origins", "role_list", "profile", "roles", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "clientId": "unionflow-mobile", + "name": "UnionFlow Mobile App", + "description": "Client pour l'application mobile UnionFlow", + "enabled": true, + "publicClient": true, + "redirectUris": ["unionflow://callback", "http://localhost:3000/callback"], + "webOrigins": ["*"], + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "fullScopeAllowed": true, + "defaultClientScopes": ["web-origins", "role_list", "profile", "roles", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + } + ], + "roles": { + "realm": [ + { + "name": "ADMIN", + "description": "Administrateur système avec tous les droits", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "PRESIDENT", + "description": "Président de l'union avec droits de gestion complète", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "SECRETAIRE", + "description": "Secrétaire avec droits de gestion des membres et événements", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "TRESORIER", + "description": "Trésorier avec droits de gestion financière", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "GESTIONNAIRE_MEMBRE", + "description": "Gestionnaire des membres avec droits de CRUD sur les membres", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "ORGANISATEUR_EVENEMENT", + "description": "Organisateur d'événements avec droits de gestion des événements", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + }, + { + "name": "MEMBRE", + "description": "Membre standard avec droits de consultation", + "composite": false, + "clientRole": false, + "containerId": "unionflow" + } + ] + }, + "users": [ + { + "username": "admin", + "enabled": true, + "emailVerified": true, + "firstName": "Administrateur", + "lastName": "Système", + "email": "admin@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "admin123", + "temporary": false + } + ], + "realmRoles": ["ADMIN", "PRESIDENT"], + "clientRoles": {} + }, + { + "username": "president", + "enabled": true, + "emailVerified": true, + "firstName": "Jean", + "lastName": "Dupont", + "email": "president@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "president123", + "temporary": false + } + ], + "realmRoles": ["PRESIDENT", "MEMBRE"], + "clientRoles": {} + }, + { + "username": "secretaire", + "enabled": true, + "emailVerified": true, + "firstName": "Marie", + "lastName": "Martin", + "email": "secretaire@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "secretaire123", + "temporary": false + } + ], + "realmRoles": ["SECRETAIRE", "GESTIONNAIRE_MEMBRE", "MEMBRE"], + "clientRoles": {} + }, + { + "username": "tresorier", + "enabled": true, + "emailVerified": true, + "firstName": "Pierre", + "lastName": "Durand", + "email": "tresorier@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "tresorier123", + "temporary": false + } + ], + "realmRoles": ["TRESORIER", "MEMBRE"], + "clientRoles": {} + }, + { + "username": "membre1", + "enabled": true, + "emailVerified": true, + "firstName": "Sophie", + "lastName": "Bernard", + "email": "membre1@unionflow.dev", + "credentials": [ + { + "type": "password", + "value": "membre123", + "temporary": false + } + ], + "realmRoles": ["MEMBRE"], + "clientRoles": {} + } + ], + "groups": [ + { + "name": "Administration", + "path": "/Administration", + "realmRoles": ["ADMIN"], + "subGroups": [] + }, + { + "name": "Bureau", + "path": "/Bureau", + "realmRoles": ["PRESIDENT", "SECRETAIRE", "TRESORIER"], + "subGroups": [] + }, + { + "name": "Gestionnaires", + "path": "/Gestionnaires", + "realmRoles": ["GESTIONNAIRE_MEMBRE", "ORGANISATEUR_EVENEMENT"], + "subGroups": [] + }, + { + "name": "Membres", + "path": "/Membres", + "realmRoles": ["MEMBRE"], + "subGroups": [] + } + ] +} diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..5572190 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,161 @@ +dev\lions\unionflow\server\entity\RolePermission$RolePermissionBuilder.class +dev\lions\unionflow\server\resource\ComptabiliteResource.class +dev\lions\unionflow\server\resource\NotificationResource.class +dev\lions\unionflow\server\resource\DocumentResource$ErrorResponse.class +dev\lions\unionflow\server\service\AnalyticsService.class +dev\lions\unionflow\server\service\DocumentService.class +dev\lions\unionflow\server\entity\MembreRole.class +dev\lions\unionflow\server\entity\WebhookWave$WebhookWaveBuilder.class +dev\lions\unionflow\server\resource\AnalyticsResource.class +dev\lions\unionflow\server\resource\EvenementResource.class +dev\lions\unionflow\server\service\NotificationHistoryService.class +dev\lions\unionflow\server\service\PermissionService.class +de\lions\unionflow\server\auth\AuthCallbackResource.class +dev\lions\unionflow\server\entity\PaiementEvenement$PaiementEvenementBuilder.class +dev\lions\unionflow\server\entity\Role$RoleBuilder.class +dev\lions\unionflow\server\entity\Evenement$EvenementBuilder.class +dev\lions\unionflow\server\service\MatchingService$ResultatMatching.class +dev\lions\unionflow\server\repository\RoleRepository.class +dev\lions\unionflow\server\entity\Organisation$OrganisationBuilder.class +dev\lions\unionflow\server\service\AuditService.class +dev\lions\unionflow\server\dto\EvenementMobileDTO$EvenementMobileDTOBuilder.class +dev\lions\unionflow\server\service\MatchingService.class +dev\lions\unionflow\server\security\SecurityConfig$Permissions.class +dev\lions\unionflow\server\entity\Adresse$AdresseBuilder.class +dev\lions\unionflow\server\entity\PaiementEvenement.class +dev\lions\unionflow\server\entity\CompteWave.class +dev\lions\unionflow\server\repository\CompteWaveRepository.class +dev\lions\unionflow\server\entity\Permission$PermissionBuilder.class +dev\lions\unionflow\server\entity\Membre.class +dev\lions\unionflow\server\repository\AdresseRepository.class +dev\lions\unionflow\server\entity\Adhesion.class +dev\lions\unionflow\server\service\NotificationHistoryService$NotificationHistoryEntry.class +dev\lions\unionflow\server\entity\Organisation.class +dev\lions\unionflow\server\service\AdhesionService.class +dev\lions\unionflow\server\service\DashboardServiceImpl.class +dev\lions\unionflow\server\service\AnalyticsService$1.class +dev\lions\unionflow\server\entity\Paiement$PaiementBuilder.class +dev\lions\unionflow\server\entity\Paiement.class +dev\lions\unionflow\server\service\PreferencesNotificationService.class +dev\lions\unionflow\server\entity\ConfigurationWave.class +dev\lions\unionflow\server\entity\PieceJointe$PieceJointeBuilder.class +dev\lions\unionflow\server\entity\Notification$NotificationBuilder.class +dev\lions\unionflow\server\repository\TypeOrganisationRepository.class +dev\lions\unionflow\server\repository\MembreRepository.class +dev\lions\unionflow\server\entity\DemandeAide.class +dev\lions\unionflow\server\repository\EcritureComptableRepository.class +dev\lions\unionflow\server\service\WaveService.class +dev\lions\unionflow\server\repository\PaiementRepository.class +dev\lions\unionflow\server\service\ComptabiliteService.class +dev\lions\unionflow\server\resource\WaveResource.class +dev\lions\unionflow\server\resource\AuditResource.class +dev\lions\unionflow\server\repository\LigneEcritureRepository.class +dev\lions\unionflow\server\security\SecurityConfig$Roles.class +dev\lions\unionflow\server\resource\MembreResource.class +dev\lions\unionflow\server\repository\AuditLogRepository.class +dev\lions\unionflow\server\service\TrendAnalysisService$TendanceDTO.class +dev\lions\unionflow\server\service\KPICalculatorService.class +dev\lions\unionflow\server\entity\TransactionWave.class +dev\lions\unionflow\server\entity\Role.class +dev\lions\unionflow\server\entity\CompteComptable$CompteComptableBuilder.class +dev\lions\unionflow\server\resource\DashboardResource.class +dev\lions\unionflow\server\entity\JournalComptable.class +dev\lions\unionflow\server\service\NotificationService.class +dev\lions\unionflow\server\entity\PaiementAide.class +dev\lions\unionflow\server\resource\NotificationResource$NotificationGroupeeRequest.class +dev\lions\unionflow\server\entity\TemplateNotification$TemplateNotificationBuilder.class +dev\lions\unionflow\server\entity\InscriptionEvenement.class +dev\lions\unionflow\server\service\DemandeAideService$1.class +dev\lions\unionflow\server\entity\Role$TypeRole.class +dev\lions\unionflow\server\entity\PaiementCotisation.class +dev\lions\unionflow\server\repository\TemplateNotificationRepository.class +dev\lions\unionflow\server\resource\PreferencesResource.class +dev\lions\unionflow\server\resource\TypeOrganisationResource.class +dev\lions\unionflow\server\service\TrendAnalysisService$1.class +dev\lions\unionflow\server\entity\CompteComptable.class +dev\lions\unionflow\server\entity\Cotisation.class +dev\lions\unionflow\server\entity\PaiementAide$PaiementAideBuilder.class +dev\lions\unionflow\server\resource\AdhesionResource.class +dev\lions\unionflow\server\entity\Document$DocumentBuilder.class +dev\lions\unionflow\server\service\MembreImportExportService$ResultatImport.class +dev\lions\unionflow\server\entity\PieceJointe.class +dev\lions\unionflow\server\service\PropositionAideService.class +dev\lions\unionflow\server\entity\WebhookWave.class +dev\lions\unionflow\server\entity\Notification.class +dev\lions\unionflow\server\entity\InscriptionEvenement$StatutInscription.class +dev\lions\unionflow\server\entity\LigneEcriture.class +dev\lions\unionflow\server\resource\ComptabiliteResource$ErrorResponse.class +dev\lions\unionflow\server\entity\LigneEcriture$LigneEcritureBuilder.class +dev\lions\unionflow\server\service\TrendAnalysisService.class +dev\lions\unionflow\server\entity\CompteWave$CompteWaveBuilder.class +dev\lions\unionflow\server\exception\JsonProcessingExceptionMapper.class +dev\lions\unionflow\server\entity\Document.class +dev\lions\unionflow\server\repository\DocumentRepository.class +dev\lions\unionflow\server\entity\MembreRole$MembreRoleBuilder.class +dev\lions\unionflow\server\service\CotisationService.class +dev\lions\unionflow\server\entity\Cotisation$CotisationBuilder.class +dev\lions\unionflow\server\repository\ConfigurationWaveRepository.class +dev\lions\unionflow\server\repository\OrganisationRepository.class +dev\lions\unionflow\server\repository\PieceJointeRepository.class +dev\lions\unionflow\server\entity\AuditLog.class +dev\lions\unionflow\server\security\SecurityConfig.class +dev\lions\unionflow\server\resource\NotificationResource$ErrorResponse.class +dev\lions\unionflow\server\resource\OrganisationResource.class +dev\lions\unionflow\server\entity\InscriptionEvenement$InscriptionEvenementBuilder.class +dev\lions\unionflow\server\entity\PaiementCotisation$PaiementCotisationBuilder.class +dev\lions\unionflow\server\service\ExportService.class +dev\lions\unionflow\server\service\RoleService.class +dev\lions\unionflow\server\service\EvenementService.class +dev\lions\unionflow\server\entity\Evenement$StatutEvenement.class +dev\lions\unionflow\server\repository\AdhesionRepository.class +dev\lions\unionflow\server\resource\HealthResource.class +dev\lions\unionflow\server\repository\NotificationRepository.class +dev\lions\unionflow\server\entity\TemplateNotification.class +dev\lions\unionflow\server\entity\PaiementAdhesion$PaiementAdhesionBuilder.class +dev\lions\unionflow\server\service\AdresseService.class +dev\lions\unionflow\server\repository\RolePermissionRepository.class +dev\lions\unionflow\server\resource\PaiementResource.class +dev\lions\unionflow\server\repository\WebhookWaveRepository.class +dev\lions\unionflow\server\resource\DocumentResource.class +dev\lions\unionflow\server\entity\PaiementAdhesion.class +dev\lions\unionflow\server\service\NotificationHistoryService$NotificationHistoryEntry$Builder.class +dev\lions\unionflow\server\entity\Permission.class +dev\lions\unionflow\server\resource\PaiementResource$ErrorResponse.class +dev\lions\unionflow\server\entity\TypeOrganisationEntity.class +dev\lions\unionflow\server\repository\BaseRepository.class +dev\lions\unionflow\server\resource\WaveResource$ErrorResponse.class +dev\lions\unionflow\server\entity\BaseEntity.class +dev\lions\unionflow\server\service\MembreImportExportService$1.class +dev\lions\unionflow\server\service\MembreImportExportService.class +dev\lions\unionflow\server\service\PaiementService.class +dev\lions\unionflow\server\service\KeycloakService.class +dev\lions\unionflow\server\entity\DemandeAide$DemandeAideBuilder.class +dev\lions\unionflow\server\util\IdConverter.class +dev\lions\unionflow\server\repository\CotisationRepository.class +dev\lions\unionflow\server\repository\DemandeAideRepository.class +dev\lions\unionflow\server\entity\EcritureComptable$EcritureComptableBuilder.class +dev\lions\unionflow\server\resource\CotisationResource.class +dev\lions\unionflow\server\entity\Adresse.class +dev\lions\unionflow\server\entity\TransactionWave$TransactionWaveBuilder.class +dev\lions\unionflow\server\entity\Evenement$TypeEvenement.class +dev\lions\unionflow\server\dto\EvenementMobileDTO.class +dev\lions\unionflow\server\entity\Adhesion$AdhesionBuilder.class +dev\lions\unionflow\server\entity\Membre$MembreBuilder.class +dev\lions\unionflow\server\repository\MembreRoleRepository.class +dev\lions\unionflow\server\resource\ExportResource.class +dev\lions\unionflow\server\service\DemandeAideService.class +dev\lions\unionflow\server\entity\RolePermission.class +dev\lions\unionflow\server\repository\TransactionWaveRepository.class +dev\lions\unionflow\server\repository\CompteComptableRepository.class +dev\lions\unionflow\server\repository\EvenementRepository.class +dev\lions\unionflow\server\entity\EcritureComptable.class +dev\lions\unionflow\server\service\MembreService.class +dev\lions\unionflow\server\service\OrganisationService.class +dev\lions\unionflow\server\service\TrendAnalysisService$StatistiquesDTO.class +dev\lions\unionflow\server\service\TypeOrganisationService.class +dev\lions\unionflow\server\entity\ConfigurationWave$ConfigurationWaveBuilder.class +dev\lions\unionflow\server\entity\Evenement.class +dev\lions\unionflow\server\entity\JournalComptable$JournalComptableBuilder.class +dev\lions\unionflow\server\repository\JournalComptableRepository.class +dev\lions\unionflow\server\repository\PermissionRepository.class +dev\lions\unionflow\server\UnionFlowServerApplication.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..70c75ae --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,109 @@ +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\AnalyticsResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\HealthResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\DashboardServiceImpl.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\NotificationService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\PieceJointe.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\CompteWaveRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\RoleRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\MembreRole.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\CompteComptable.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\de\lions\unionflow\server\auth\AuthCallbackResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\WebhookWave.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\MembreImportExportService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\EcritureComptableRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\ConfigurationWave.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\UnionFlowServerApplication.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\TrendAnalysisService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Paiement.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\AdhesionRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\AuditService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\ConfigurationWaveRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\AuditResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\PaiementAdhesion.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\CotisationResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\NotificationHistoryService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\DemandeAide.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\RolePermissionRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\exception\JsonProcessingExceptionMapper.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\InscriptionEvenement.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\MembreRoleRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\DemandeAideRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Membre.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\PermissionRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\EvenementResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\OrganisationService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\OrganisationRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\PaiementResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\PropositionAideService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\OrganisationResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\AdresseService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\AuditLog.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\EvenementService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\ComptabiliteResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\PieceJointeRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\BaseEntity.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Permission.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\security\SecurityConfig.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\RoleService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\PreferencesNotificationService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\PaiementCotisation.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Organisation.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\PaiementAide.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Notification.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\PaiementRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\AdhesionService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\RolePermission.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\CompteWave.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\BaseRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\TransactionWaveRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\CotisationRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\PaiementService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\DocumentRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\PreferencesResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Adhesion.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\LigneEcriture.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\ExportResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\TemplateNotificationRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\MembreResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\PaiementEvenement.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\AuditLogRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\DocumentResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\DemandeAideService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Role.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\WebhookWaveRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\AnalyticsService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\CompteComptableRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Adresse.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\AdhesionResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\DocumentService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\AdresseRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\LigneEcritureRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\JournalComptable.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\TypeOrganisationService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\TypeOrganisationRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\MatchingService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\ComptabiliteService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\NotificationResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Document.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\TypeOrganisationResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\KPICalculatorService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\dto\EvenementMobileDTO.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\PermissionService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Evenement.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\DashboardResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\TransactionWave.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\MembreService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\EcritureComptable.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\TemplateNotification.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\JournalComptableRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\CotisationService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\WaveService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\resource\WaveResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\TypeOrganisationEntity.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\KeycloakService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\EvenementRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\util\IdConverter.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\service\ExportService.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\MembreRepository.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\entity\Cotisation.java +C:\Users\dadyo\PersonalProjects\lions-workspace\unionflow\unionflow-server-impl-quarkus\src\main\java\dev\lions\unionflow\server\repository\NotificationRepository.java diff --git a/target/test-classes/dev/lions/unionflow/server/resource/EvenementResourceTest.class b/target/test-classes/dev/lions/unionflow/server/resource/EvenementResourceTest.class new file mode 100644 index 0000000..9b086f3 Binary files /dev/null and b/target/test-classes/dev/lions/unionflow/server/resource/EvenementResourceTest.class differ diff --git a/target/test-classes/dev/lions/unionflow/server/resource/MembreResourceAdvancedSearchTest.class b/target/test-classes/dev/lions/unionflow/server/resource/MembreResourceAdvancedSearchTest.class new file mode 100644 index 0000000..40c6eb3 Binary files /dev/null and b/target/test-classes/dev/lions/unionflow/server/resource/MembreResourceAdvancedSearchTest.class differ diff --git a/target/test-classes/dev/lions/unionflow/server/resource/MembreResourceImportExportTest.class b/target/test-classes/dev/lions/unionflow/server/resource/MembreResourceImportExportTest.class new file mode 100644 index 0000000..09f9554 Binary files /dev/null and b/target/test-classes/dev/lions/unionflow/server/resource/MembreResourceImportExportTest.class differ diff --git a/target/test-classes/dev/lions/unionflow/server/resource/OrganisationResourceTest.class b/target/test-classes/dev/lions/unionflow/server/resource/OrganisationResourceTest.class new file mode 100644 index 0000000..91af3bf Binary files /dev/null and b/target/test-classes/dev/lions/unionflow/server/resource/OrganisationResourceTest.class differ diff --git a/target/test-classes/dev/lions/unionflow/server/service/MembreImportExportServiceTest.class b/target/test-classes/dev/lions/unionflow/server/service/MembreImportExportServiceTest.class new file mode 100644 index 0000000..10021e4 Binary files /dev/null and b/target/test-classes/dev/lions/unionflow/server/service/MembreImportExportServiceTest.class differ diff --git a/target/test-classes/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.class b/target/test-classes/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.class new file mode 100644 index 0000000..5b8f4ec Binary files /dev/null and b/target/test-classes/dev/lions/unionflow/server/service/MembreServiceAdvancedSearchTest.class differ