Compare commits

...

1 Commits

127 changed files with 17488 additions and 9557 deletions

34
pom.xml
View File

@@ -14,9 +14,9 @@
<!-- Versions --> <!-- Versions -->
<compiler-plugin.version>3.13.0</compiler-plugin.version> <compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release> <maven.compiler.release>17</maven.compiler.release>
<myfaces.version>4.0.1</myfaces.version> <myfaces.version>4.0.2</myfaces.version>
<primefaces.version>13.0.5</primefaces.version> <primefaces.version>14.0.0</primefaces.version>
<quarkus.platform.version>3.7.3</quarkus.platform.version> <quarkus.platform.version>3.27.3</quarkus.platform.version>
<lombok.version>1.18.32</lombok.version> <lombok.version>1.18.32</lombok.version>
<jackson.version>2.17.0</jackson.version> <jackson.version>2.17.0</jackson.version>
@@ -85,12 +85,23 @@
<dependency> <dependency>
<groupId>io.quarkiverse.primefaces</groupId> <groupId>io.quarkiverse.primefaces</groupId>
<artifactId>quarkus-primefaces</artifactId> <artifactId>quarkus-primefaces</artifactId>
<version>3.14.0</version> <version>3.15.1</version>
</dependency>
<dependency>
<groupId>org.primefaces</groupId>
<artifactId>primefaces</artifactId>
<version>14.0.0</version>
<classifier>jakarta</classifier>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.myfaces.core.extensions.quarkus</groupId> <groupId>org.apache.myfaces.core.extensions.quarkus</groupId>
<artifactId>myfaces-quarkus</artifactId> <artifactId>myfaces-quarkus</artifactId>
<version>4.0.1</version> <version>4.0.2</version>
</dependency>
<dependency>
<groupId>dev.lions</groupId>
<artifactId>primefaces-freya-extension</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency> </dependency>
<!-- Persistence --> <!-- Persistence -->
@@ -123,6 +134,13 @@
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-mailer</artifactId> <artifactId>quarkus-mailer</artifactId>
</dependency> </dependency>
<!-- PDF Generation -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets</artifactId> <artifactId>quarkus-websockets</artifactId>
@@ -197,7 +215,11 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.quarkus</groupId> <groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId> <artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency> </dependency>
<!-- Sécurité --> <!-- Sécurité -->
<dependency> <dependency>

View File

@@ -1,175 +1,175 @@
# Déclaration des services pour votre application Quarkus avec PostgreSQL, pgAdmin, Prometheus, Grafana et les exporters. # Déclaration des services pour votre application Quarkus avec PostgreSQL, pgAdmin, Prometheus, Grafana et les exporters.
services: services:
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Service principal : Application Quarkus # Service principal : Application Quarkus
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
quarkus-app: quarkus-app:
container_name: ${APP_NAME}-app container_name: ${APP_NAME}-app
image: dahoudg/lionsdev-client-impl-quarkus-jvm:latest image: dahoudg/lionsdev-client-impl-quarkus-jvm:latest
build: build:
context: ./ context: ./
dockerfile: Dockerfile.jvm dockerfile: Dockerfile.jvm
args: args:
- JAVA_VERSION=${JAVA_VERSION} - JAVA_VERSION=${JAVA_VERSION}
environment: environment:
# Configuration de la base de données # Configuration de la base de données
- QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
- QUARKUS_DATASOURCE_USERNAME=${POSTGRES_USER} - QUARKUS_DATASOURCE_USERNAME=${POSTGRES_USER}
- QUARKUS_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD} - QUARKUS_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD}
# Configuration du serveur # Configuration du serveur
- QUARKUS_HTTP_PORT=${QUARKUS_HTTP_PORT} - QUARKUS_HTTP_PORT=${QUARKUS_HTTP_PORT}
- TZ=${TZ} - TZ=${TZ}
# Configuration des chemins et stockage # Configuration des chemins et stockage
- APP_STORAGE_BASE_PATH=/app/storage - APP_STORAGE_BASE_PATH=/app/storage
- APP_BASE_URL=${APP_BASE_URL:-http://localhost:8080} - APP_BASE_URL=${APP_BASE_URL:-http://localhost:8080}
- APP_ENVIRONMENT=${ENVIRONMENT:-production} - APP_ENVIRONMENT=${ENVIRONMENT:-production}
# Configuration des logs # Configuration des logs
- QUARKUS_LOG_FILE_ENABLE=true - QUARKUS_LOG_FILE_ENABLE=true
- QUARKUS_LOG_FILE_PATH=/var/log/lionsdev/application.log - QUARKUS_LOG_FILE_PATH=/var/log/lionsdev/application.log
- QUARKUS_LOG_LEVEL=INFO - QUARKUS_LOG_LEVEL=INFO
volumes: volumes:
- ./logs:/var/log/lionsdev - ./logs:/var/log/lionsdev
- ./storage:/app/storage - ./storage:/app/storage
tmpfs: tmpfs:
- /tmp - /tmp
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:${QUARKUS_HTTP_PORT}/q/health"] test: ["CMD", "curl", "-f", "http://localhost:${QUARKUS_HTTP_PORT}/q/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
ports: ports:
- "${QUARKUS_HTTP_PORT}:8080" # Expose uniquement le port nécessaire pour Quarkus - "${QUARKUS_HTTP_PORT}:8080" # Expose uniquement le port nécessaire pour Quarkus
deploy: deploy:
resources: resources:
limits: limits:
cpus: '${QUARKUS_CPU_LIMIT}' cpus: '${QUARKUS_CPU_LIMIT}'
memory: ${QUARKUS_MEMORY_LIMIT} memory: ${QUARKUS_MEMORY_LIMIT}
depends_on: depends_on:
postgres-db: postgres-db:
condition: service_healthy condition: service_healthy
networks: networks:
- app-network - app-network
restart: unless-stopped restart: unless-stopped
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Base de données : PostgreSQL # Base de données : PostgreSQL
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
postgres-db: postgres-db:
container_name: ${APP_NAME}-db container_name: ${APP_NAME}-db
image: postgres:16-alpine image: postgres:16-alpine
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: ${TZ} TZ: ${TZ}
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - postgres-data:/var/lib/postgresql/data
ports: ports:
- "5432:5432" - "5432:5432"
deploy: deploy:
resources: resources:
limits: limits:
cpus: '1.0' cpus: '1.0'
memory: 1G memory: 1G
networks: networks:
- app-network - app-network
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Interface d'administration PostgreSQL : pgAdmin # Interface d'administration PostgreSQL : pgAdmin
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
pgadmin: pgadmin:
container_name: ${APP_NAME}-pgadmin container_name: ${APP_NAME}-pgadmin
image: dpage/pgadmin4:latest image: dpage/pgadmin4:latest
restart: unless-stopped restart: unless-stopped
environment: environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL} PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL}
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
TZ: ${TZ} TZ: ${TZ}
ports: ports:
- "${PGADMIN_PORT}:80" - "${PGADMIN_PORT}:80"
volumes: volumes:
- pgadmin-data:/var/lib/pgadmin - pgadmin-data:/var/lib/pgadmin
depends_on: depends_on:
postgres-db: postgres-db:
condition: service_healthy condition: service_healthy
networks: networks:
- app-network - app-network
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Monitoring : Prometheus # Monitoring : Prometheus
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
prometheus: prometheus:
container_name: ${APP_NAME}-prometheus container_name: ${APP_NAME}-prometheus
image: prom/prometheus:latest image: prom/prometheus:latest
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus - prometheus-data:/prometheus
ports: ports:
- "${PROMETHEUS_PORT}:9090" - "${PROMETHEUS_PORT}:9090"
depends_on: depends_on:
- postgres-exporter - postgres-exporter
- node-exporter - node-exporter
networks: networks:
- app-network - app-network
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Exporters pour PostgreSQL et le serveur # Exporters pour PostgreSQL et le serveur
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
postgres-exporter: postgres-exporter:
container_name: ${APP_NAME}-postgres-exporter container_name: ${APP_NAME}-postgres-exporter
image: prometheuscommunity/postgres-exporter:latest image: prometheuscommunity/postgres-exporter:latest
environment: environment:
DATA_SOURCE_NAME: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres-db:5432/${POSTGRES_DB}?sslmode=disable" DATA_SOURCE_NAME: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres-db:5432/${POSTGRES_DB}?sslmode=disable"
ports: ports:
- "${POSTGRES_EXPORTER_PORT}:9187" - "${POSTGRES_EXPORTER_PORT}:9187"
depends_on: depends_on:
postgres-db: postgres-db:
condition: service_healthy condition: service_healthy
networks: networks:
- app-network - app-network
node-exporter: node-exporter:
container_name: ${APP_NAME}-node-exporter container_name: ${APP_NAME}-node-exporter
image: prom/node-exporter:latest image: prom/node-exporter:latest
ports: ports:
- "${NODE_EXPORTER_PORT}:9100" - "${NODE_EXPORTER_PORT}:9100"
networks: networks:
- app-network - app-network
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# Visualisation : Grafana # Visualisation : Grafana
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
grafana: grafana:
container_name: ${APP_NAME}-grafana container_name: ${APP_NAME}-grafana
image: grafana/grafana:latest image: grafana/grafana:latest
restart: unless-stopped restart: unless-stopped
environment: environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER} GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD} GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
TZ: ${TZ} TZ: ${TZ}
ports: ports:
- "${GRAFANA_PORT}:3000" - "${GRAFANA_PORT}:3000"
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana
depends_on: depends_on:
- prometheus - prometheus
networks: networks:
- app-network - app-network
volumes: volumes:
postgres-data: postgres-data:
pgadmin-data: pgadmin-data:
prometheus-data: prometheus-data:
grafana-data: grafana-data:
networks: networks:
app-network: app-network:
driver: bridge driver: bridge

View File

@@ -1,11 +1,11 @@
-- Création de la base de données si elle n'existe pas -- Création de la base de données si elle n'existe pas
CREATE DATABASE IF NOT EXISTS lionsdev_db; CREATE DATABASE IF NOT EXISTS lionsdev_db;
-- Configuration des droits d'accès -- Configuration des droits d'accès
ALTER DATABASE lionsdev_db OWNER TO lions_admin_db; ALTER DATABASE lionsdev_db OWNER TO lions_admin_db;
GRANT ALL PRIVILEGES ON DATABASE lionsdev_db TO lions_admin_db; GRANT ALL PRIVILEGES ON DATABASE lionsdev_db TO lions_admin_db;
-- Configuration des schémas nécessaires -- Configuration des schémas nécessaires
\c lionsdev_db \c lionsdev_db
CREATE SCHEMA IF NOT EXISTS public; CREATE SCHEMA IF NOT EXISTS public;
GRANT ALL ON SCHEMA public TO lions_admin_db; GRANT ALL ON SCHEMA public TO lions_admin_db;

View File

@@ -1,29 +1,29 @@
events {} events {}
http { http {
server { server {
listen 80; listen 80;
server_name lions.dev www.lions.dev; server_name lions.dev www.lions.dev;
location / { location / {
proxy_pass http://quarkus-app:8080; proxy_pass http://quarkus-app:8080;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
} }
server { server {
listen 80; listen 80;
server_name pgadmin.lions.dev; server_name pgadmin.lions.dev;
location / { location / {
proxy_pass http://pgadmin:80; proxy_pass http://pgadmin:80;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
} }
} }

View File

@@ -1,61 +1,61 @@
# Configuration Prometheus corrigée # Configuration Prometheus corrigée
global: global:
scrape_interval: 15s scrape_interval: 15s
evaluation_interval: 15s evaluation_interval: 15s
external_labels: external_labels:
monitor: 'lions-portal-monitor' monitor: 'lions-portal-monitor'
rule_files: rule_files:
- "rules/*rules.yml" - "rules/*rules.yml"
- "alerts/*alerts.yml" - "alerts/*alerts.yml"
alerting: alerting:
alertmanagers: alertmanagers:
- static_configs: - static_configs:
- targets: - targets:
- 'alertmanager:9093' - 'alertmanager:9093'
scrape_configs: scrape_configs:
# Application Quarkus metrics # Application Quarkus metrics
- job_name: 'quarkus' - job_name: 'quarkus'
metrics_path: '/q/metrics' metrics_path: '/q/metrics'
static_configs: static_configs:
- targets: ['quarkus-app:8080'] - targets: ['quarkus-app:8080']
scrape_interval: 10s scrape_interval: 10s
# Postgres Exporter metrics # Postgres Exporter metrics
- job_name: 'postgres' - job_name: 'postgres'
static_configs: static_configs:
- targets: ['postgres-exporter:9187'] - targets: ['postgres-exporter:9187']
# Nginx metrics # Nginx metrics
- job_name: 'nginx' - job_name: 'nginx'
static_configs: static_configs:
- targets: ['nginx-exporter:9113'] - targets: ['nginx-exporter:9113']
# Node Exporter metrics (system metrics) # Node Exporter metrics (system metrics)
- job_name: 'node' - job_name: 'node'
static_configs: static_configs:
- targets: ['node-exporter:9100'] - targets: ['node-exporter:9100']
# Prometheus self-monitoring # Prometheus self-monitoring
- job_name: 'prometheus' - job_name: 'prometheus'
static_configs: static_configs:
- targets: ['localhost:9090'] - targets: ['localhost:9090']
# Nginx Status Page # Nginx Status Page
- job_name: 'nginx-status' - job_name: 'nginx-status'
metrics_path: /stub_status metrics_path: /stub_status
static_configs: static_configs:
- targets: ['nginx:80'] - targets: ['nginx:80']
# Grafana metrics # Grafana metrics
- job_name: 'grafana' - job_name: 'grafana'
static_configs: static_configs:
- targets: ['grafana:3000'] - targets: ['grafana:3000']
# Node Exporter Host metrics # Node Exporter Host metrics
- job_name: 'node_exporter_host' - job_name: 'node_exporter_host'
static_configs: static_configs:
- targets: ['node-exporter:9100'] - targets: ['node-exporter:9100']
labels: labels:
instance: 'host' instance: 'host'

View File

@@ -0,0 +1,393 @@
package dev.lions.audit;
import dev.lions.quote.ModuleCatalog;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import io.quarkus.runtime.StartupEvent;
import java.util.Arrays;
/**
* Initialisation des questions d'audit spécifiques aux PME ivoiriennes
*/
@ApplicationScoped
public class AuditDataInitializer {
@Inject
EntityManager em;
@Transactional
public void initializeAuditQuestions(@Observes StartupEvent event) {
// Vérifier si les questions existent déjà
Long questionCount = em.createQuery("SELECT COUNT(q) FROM AuditQuestion q", Long.class)
.getSingleResult();
if (questionCount == 0) {
createCommercialQuestions();
createStockQuestions();
createComptabiliteQuestions();
createRhQuestions();
createInfrastructureQuestions();
}
// Vérifier si le catalogue existe déjà
Long catalogCount = em.createQuery("SELECT COUNT(m) FROM ModuleCatalog m", Long.class)
.getSingleResult();
if (catalogCount == 0) {
createModuleCatalog();
}
}
private void createCommercialQuestions() {
// Question 1: Gestion des clients
AuditQuestion q1 = new AuditQuestion();
q1.setCategory("commercial");
q1.setQuestion("Comment gérez-vous actuellement vos contacts clients ?");
q1.setOptions(Arrays.asList(
"Carnet papier ou fichier Excel basique",
"Fichier Excel avec historique des contacts",
"Logiciel de gestion simple (contacts + ventes)",
"CRM complet avec suivi des opportunités",
"CRM avancé avec automation marketing"
));
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
q1.setWeight(3);
q1.setDisplayOrder(1);
q1.setRecommendation("Un CRM adapté améliore la relation client et augmente les ventes de 20-30%");
em.persist(q1);
// Question 2: Facturation
AuditQuestion q2 = new AuditQuestion();
q2.setCategory("commercial");
q2.setQuestion("Comment établissez-vous vos factures ?");
q2.setOptions(Arrays.asList(
"Factures manuscrites ou Word/Excel",
"Modèle Excel avec calculs automatiques",
"Logiciel de facturation simple",
"Système intégré (devis → facture → paiement)",
"Facturation automatisée avec relances"
));
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
q2.setWeight(3);
q2.setDisplayOrder(2);
q2.setRecommendation("La facturation automatisée réduit les erreurs et accélère les paiements");
em.persist(q2);
// Question 3: Suivi des ventes
AuditQuestion q3 = new AuditQuestion();
q3.setCategory("commercial");
q3.setQuestion("Avez-vous une visibilité sur vos performances commerciales ?");
q3.setOptions(Arrays.asList(
"Aucun suivi régulier",
"Calculs manuels mensuels",
"Tableaux de bord Excel",
"Rapports automatiques hebdomadaires",
"Dashboard temps réel avec KPI"
));
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
q3.setWeight(2);
q3.setDisplayOrder(3);
em.persist(q3);
// Question 4: Gestion des devis
AuditQuestion q4 = new AuditQuestion();
q4.setCategory("commercial");
q4.setQuestion("Comment gérez-vous vos devis et propositions commerciales ?");
q4.setOptions(Arrays.asList(
"Devis manuscrits ou Word",
"Modèles Excel personnalisables",
"Logiciel de devis avec bibliothèque",
"Système intégré avec signature électronique",
"Configurateur automatique avec approbation workflow"
));
q4.setScores(Arrays.asList(1, 2, 3, 4, 5));
q4.setWeight(2);
q4.setDisplayOrder(4);
em.persist(q4);
}
private void createStockQuestions() {
// Question 1: Inventaire
AuditQuestion q1 = new AuditQuestion();
q1.setCategory("stock");
q1.setQuestion("Comment suivez-vous vos stocks ?");
q1.setOptions(Arrays.asList(
"Comptage manuel périodique",
"Fichier Excel mis à jour manuellement",
"Logiciel de stock simple",
"Système avec codes-barres",
"Gestion automatisée avec alertes"
));
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
q1.setWeight(3);
q1.setDisplayOrder(1);
em.persist(q1);
// Question 2: Approvisionnement
AuditQuestion q2 = new AuditQuestion();
q2.setCategory("stock");
q2.setQuestion("Comment planifiez-vous vos achats et approvisionnements ?");
q2.setOptions(Arrays.asList(
"Achats au feeling quand stock bas",
"Planning mensuel basique",
"Calculs de stock minimum",
"Prévisions basées sur l'historique",
"Optimisation automatique avec IA"
));
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
q2.setWeight(2);
q2.setDisplayOrder(2);
em.persist(q2);
// Question 3: Valorisation
AuditQuestion q3 = new AuditQuestion();
q3.setCategory("stock");
q3.setQuestion("Connaissez-vous la valeur exacte de votre stock ?");
q3.setOptions(Arrays.asList(
"Estimation approximative",
"Calcul manuel périodique",
"Valorisation Excel",
"Valorisation automatique temps réel",
"Analyse de rotation et obsolescence"
));
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
q3.setWeight(2);
q3.setDisplayOrder(3);
em.persist(q3);
}
private void createComptabiliteQuestions() {
// Question 1: Tenue comptable
AuditQuestion q1 = new AuditQuestion();
q1.setCategory("comptabilite");
q1.setQuestion("Comment tenez-vous votre comptabilité ?");
q1.setOptions(Arrays.asList(
"Cahiers manuscrits",
"Excel avec saisie manuelle",
"Logiciel comptable simple",
"Logiciel intégré avec automatisation",
"ERP complet avec expert-comptable connecté"
));
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
q1.setWeight(3);
q1.setDisplayOrder(1);
em.persist(q1);
// Question 2: Déclarations fiscales
AuditQuestion q2 = new AuditQuestion();
q2.setCategory("comptabilite");
q2.setQuestion("Comment gérez-vous vos obligations fiscales (TVA, IS, etc.) ?");
q2.setOptions(Arrays.asList(
"Calculs manuels avec expert-comptable",
"Excel avec formules de calcul",
"Logiciel avec aide au calcul",
"Génération automatique des déclarations",
"Télédéclaration automatisée"
));
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
q2.setWeight(3);
q2.setDisplayOrder(2);
em.persist(q2);
// Question 3: Analyse financière
AuditQuestion q3 = new AuditQuestion();
q3.setCategory("comptabilite");
q3.setQuestion("Avez-vous une vision claire de votre situation financière ?");
q3.setOptions(Arrays.asList(
"Bilan annuel uniquement",
"Situation trimestrielle",
"Tableaux de bord mensuels",
"Reporting automatique hebdomadaire",
"Dashboard temps réel avec prévisions"
));
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
q3.setWeight(2);
q3.setDisplayOrder(3);
em.persist(q3);
}
private void createRhQuestions() {
// Question 1: Gestion du personnel
AuditQuestion q1 = new AuditQuestion();
q1.setCategory("rh");
q1.setQuestion("Comment gérez-vous les dossiers de vos employés ?");
q1.setOptions(Arrays.asList(
"Dossiers papier classés",
"Fichiers Excel par employé",
"Logiciel RH basique",
"SIRH avec self-service employé",
"SIRH complet avec workflow"
));
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
q1.setWeight(2);
q1.setDisplayOrder(1);
em.persist(q1);
// Question 2: Paie et CNPS
AuditQuestion q2 = new AuditQuestion();
q2.setCategory("rh");
q2.setQuestion("Comment calculez-vous la paie et les cotisations CNPS ?");
q2.setOptions(Arrays.asList(
"Calculs manuels",
"Excel avec formules",
"Logiciel de paie simple",
"Paie automatisée avec déclarations",
"Intégration complète CNPS/DGI"
));
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
q2.setWeight(3);
q2.setDisplayOrder(2);
em.persist(q2);
// Question 3: Gestion des congés
AuditQuestion q3 = new AuditQuestion();
q3.setCategory("rh");
q3.setQuestion("Comment gérez-vous les demandes de congés ?");
q3.setOptions(Arrays.asList(
"Demandes papier",
"Email et validation manuelle",
"Fichier Excel partagé",
"Système de workflow digital",
"Application mobile avec approbation"
));
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
q3.setWeight(1);
q3.setDisplayOrder(3);
em.persist(q3);
}
private void createInfrastructureQuestions() {
// Question 1: Équipement informatique
AuditQuestion q1 = new AuditQuestion();
q1.setCategory("infrastructure");
q1.setQuestion("Quel est l'état de votre parc informatique ?");
q1.setOptions(Arrays.asList(
"Ordinateurs anciens (>5 ans), pas de réseau",
"Mix d'ordinateurs, réseau basique",
"Parc récent, réseau WiFi, serveur local",
"Infrastructure moderne, cloud hybride",
"Infrastructure cloud-native sécurisée"
));
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
q1.setWeight(2);
q1.setDisplayOrder(1);
em.persist(q1);
// Question 2: Sauvegardes
AuditQuestion q2 = new AuditQuestion();
q2.setCategory("infrastructure");
q2.setQuestion("Comment protégez-vous vos données importantes ?");
q2.setOptions(Arrays.asList(
"Aucune sauvegarde régulière",
"Copies manuelles sur clé USB",
"Sauvegarde externe périodique",
"Sauvegarde automatique cloud",
"Sauvegarde redondante avec plan de reprise"
));
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
q2.setWeight(3);
q2.setDisplayOrder(2);
em.persist(q2);
// Question 3: Sécurité
AuditQuestion q3 = new AuditQuestion();
q3.setCategory("infrastructure");
q3.setQuestion("Quelles mesures de sécurité informatique avez-vous ?");
q3.setOptions(Arrays.asList(
"Antivirus basique uniquement",
"Antivirus + mots de passe",
"Sécurité réseau + formation utilisateurs",
"Sécurité multicouche + monitoring",
"Sécurité enterprise avec audit régulier"
));
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
q3.setWeight(3);
q3.setDisplayOrder(3);
em.persist(q3);
}
/**
* Crée le catalogue des modules avec tarification
*/
private void createModuleCatalog() {
// Module CRM Commercial
ModuleCatalog crmModule = new ModuleCatalog("CRM", "Gestion Commerciale CRM", "commercial");
crmModule.setDescription("Solution complète de gestion de la relation client avec suivi des prospects, opportunités et ventes");
crmModule.setFeatures("• Gestion contacts et prospects\n• Pipeline de ventes\n• Suivi des opportunités\n• Facturation intégrée\n• Rapports commerciaux\n• Tableaux de bord");
crmModule.setBasicPrice(150000.0); // 150K FCFA
crmModule.setStandardPrice(250000.0); // 250K FCFA
crmModule.setAdvancedPrice(400000.0); // 400K FCFA
crmModule.setEnterprisePrice(650000.0); // 650K FCFA
crmModule.setBaseImplementationDays(10);
crmModule.setMaxUsers(50);
crmModule.setSupportLevel("Email + Téléphone");
crmModule.setTechnicalRequirements("Windows/Mac/Linux, Navigateur web moderne, 4GB RAM minimum");
crmModule.setDisplayOrder(1);
crmModule.setPopular(true);
em.persist(crmModule);
// Module Gestion de Stock
ModuleCatalog stockModule = new ModuleCatalog("STOCK", "Gestion des Stocks", "stock");
stockModule.setDescription("Système de gestion des stocks avec codes-barres, inventaires et approvisionnements automatisés");
stockModule.setFeatures("• Gestion multi-entrepôts\n• Codes-barres et QR codes\n• Inventaires automatisés\n• Alertes stock minimum\n• Prévisions d'achat\n• Valorisation FIFO/LIFO");
stockModule.setBasicPrice(120000.0);
stockModule.setStandardPrice(200000.0);
stockModule.setAdvancedPrice(350000.0);
stockModule.setEnterprisePrice(550000.0);
stockModule.setBaseImplementationDays(8);
stockModule.setMaxUsers(20);
stockModule.setSupportLevel("Email + Téléphone");
stockModule.setTechnicalRequirements("Windows/Mac/Linux, Lecteur codes-barres (optionnel), Imprimante étiquettes");
stockModule.setDisplayOrder(2);
em.persist(stockModule);
// Module Comptabilité
ModuleCatalog comptaModule = new ModuleCatalog("COMPTA", "Comptabilité Intégrée", "comptabilite");
comptaModule.setDescription("Comptabilité complète conforme aux normes ivoiriennes avec déclarations fiscales automatisées");
comptaModule.setFeatures("• Plan comptable SYSCOHADA\n• Saisie automatisée\n• Déclarations TVA/IS\n• Bilan et compte de résultat\n• Rapports DGI\n• Intégration bancaire");
comptaModule.setBasicPrice(180000.0);
comptaModule.setStandardPrice(300000.0);
comptaModule.setAdvancedPrice(500000.0);
comptaModule.setEnterprisePrice(800000.0);
comptaModule.setBaseImplementationDays(12);
comptaModule.setMaxUsers(10);
comptaModule.setSupportLevel("Email + Téléphone + Expert-comptable");
comptaModule.setTechnicalRequirements("Windows/Mac/Linux, Connexion internet sécurisée, Sauvegarde automatique");
comptaModule.setDisplayOrder(3);
comptaModule.setPopular(true);
em.persist(comptaModule);
// Module RH
ModuleCatalog rhModule = new ModuleCatalog("RH", "Gestion des Ressources Humaines", "rh");
rhModule.setDescription("SIRH complet avec paie, congés, formation et conformité CNPS");
rhModule.setFeatures("• Dossiers employés\n• Calcul de paie CNPS\n• Gestion des congés\n• Formation et évaluation\n• Déclarations sociales\n• Self-service employé");
rhModule.setBasicPrice(100000.0);
rhModule.setStandardPrice(180000.0);
rhModule.setAdvancedPrice(320000.0);
rhModule.setEnterprisePrice(500000.0);
rhModule.setBaseImplementationDays(6);
rhModule.setMaxUsers(100);
rhModule.setSupportLevel("Email + Téléphone");
rhModule.setTechnicalRequirements("Windows/Mac/Linux, Connexion CNPS (optionnelle), Scanner documents");
rhModule.setDisplayOrder(4);
em.persist(rhModule);
// Module Infrastructure
ModuleCatalog infraModule = new ModuleCatalog("INFRA", "Infrastructure IT", "infrastructure");
infraModule.setDescription("Mise en place et sécurisation de l'infrastructure informatique avec sauvegarde cloud");
infraModule.setFeatures("• Audit infrastructure\n• Sécurisation réseau\n• Sauvegarde automatique\n• Antivirus enterprise\n• Monitoring 24/7\n• Support technique");
infraModule.setBasicPrice(200000.0);
infraModule.setStandardPrice(350000.0);
infraModule.setAdvancedPrice(600000.0);
infraModule.setEnterprisePrice(1000000.0);
infraModule.setBaseImplementationDays(5);
infraModule.setMaxUsers(999);
infraModule.setSupportLevel("Email + Téléphone + Intervention sur site");
infraModule.setTechnicalRequirements("Réseau existant, Serveur ou Cloud, Postes de travail Windows/Mac");
infraModule.setDisplayOrder(5);
em.persist(infraModule);
}
}

View File

@@ -0,0 +1,86 @@
package dev.lions.audit;
import jakarta.persistence.*;
import java.util.List;
/**
* Question d'audit pour évaluer la maturité digitale des PME
*/
@Entity
@Table(name = "audit_questions")
public class AuditQuestion {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String category; // "commercial", "stock", "comptabilite", "rh", "infrastructure"
@Column(nullable = false, length = 500)
private String question;
@Column(name = "question_en", length = 500)
private String questionEn; // Version anglaise
@ElementCollection
@CollectionTable(name = "audit_question_options")
private List<String> options;
@ElementCollection
@CollectionTable(name = "audit_question_scores")
private List<Integer> scores; // Score pour chaque option
@Column(nullable = false)
private Integer weight = 1; // Poids de la question
@Column(length = 1000)
private String recommendation; // Recommandation selon la réponse
@Column(nullable = false)
private Boolean active = true;
@Column(name = "display_order")
private Integer displayOrder;
// Constructeurs
public AuditQuestion() {}
public AuditQuestion(String category, String question, List<String> options, List<Integer> scores) {
this.category = category;
this.question = question;
this.options = options;
this.scores = scores;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getQuestion() { return question; }
public void setQuestion(String question) { this.question = question; }
public String getQuestionEn() { return questionEn; }
public void setQuestionEn(String questionEn) { this.questionEn = questionEn; }
public List<String> getOptions() { return options; }
public void setOptions(List<String> options) { this.options = options; }
public List<Integer> getScores() { return scores; }
public void setScores(List<Integer> scores) { this.scores = scores; }
public Integer getWeight() { return weight; }
public void setWeight(Integer weight) { this.weight = weight; }
public String getRecommendation() { return recommendation; }
public void setRecommendation(String recommendation) { this.recommendation = recommendation; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
public Integer getDisplayOrder() { return displayOrder; }
public void setDisplayOrder(Integer displayOrder) { this.displayOrder = displayOrder; }
}

View File

@@ -0,0 +1,305 @@
package dev.lions.audit;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;
import java.io.ByteArrayOutputStream;
import java.time.format.DateTimeFormatter;
import java.util.Map;
/**
* Service de génération de rapports PDF d'audit
*/
@ApplicationScoped
public class AuditReportService {
@Inject
Mailer mailer;
private static final Font TITLE_FONT = new Font(Font.FontFamily.HELVETICA, 18, Font.BOLD, BaseColor.DARK_GRAY);
private static final Font HEADER_FONT = new Font(Font.FontFamily.HELVETICA, 14, Font.BOLD, BaseColor.BLACK);
private static final Font NORMAL_FONT = new Font(Font.FontFamily.HELVETICA, 11, Font.NORMAL, BaseColor.BLACK);
private static final Font SMALL_FONT = new Font(Font.FontFamily.HELVETICA, 9, Font.NORMAL, BaseColor.GRAY);
/**
* Génère le rapport PDF d'audit
*/
public byte[] generateAuditReport(AuditResponse audit) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
PdfWriter writer = PdfWriter.getInstance(document, baos);
document.open();
// En-tête Lions Dev
addHeader(document);
// Informations entreprise
addCompanyInfo(document, audit);
// Résumé exécutif
addExecutiveSummary(document, audit);
// Scores par catégorie
addCategoryScores(document, audit);
// Recommandations détaillées
addRecommendations(document, audit);
// Estimation budgétaire
addBudgetEstimation(document, audit);
// Prochaines étapes
addNextSteps(document);
// Pied de page
addFooter(document);
document.close();
return baos.toByteArray();
}
private void addHeader(Document document) throws DocumentException {
// Logo et titre Lions Dev
Paragraph title = new Paragraph("LIONS DEV", TITLE_FONT);
title.setAlignment(Element.ALIGN_CENTER);
title.setSpacingAfter(10);
document.add(title);
Paragraph subtitle = new Paragraph("Audit de Maturité Digitale", HEADER_FONT);
subtitle.setAlignment(Element.ALIGN_CENTER);
subtitle.setSpacingAfter(20);
document.add(subtitle);
// Ligne de séparation
com.itextpdf.text.pdf.draw.LineSeparator line = new com.itextpdf.text.pdf.draw.LineSeparator();
document.add(new Chunk(line));
document.add(Chunk.NEWLINE);
}
private void addCompanyInfo(Document document, AuditResponse audit) throws DocumentException {
Paragraph section = new Paragraph("INFORMATIONS ENTREPRISE", HEADER_FONT);
section.setSpacingBefore(10);
section.setSpacingAfter(10);
document.add(section);
PdfPTable table = new PdfPTable(2);
table.setWidthPercentage(100);
table.setSpacingAfter(15);
addTableRow(table, "Entreprise:", audit.getCompanyName());
addTableRow(table, "Contact:", audit.getContactName());
addTableRow(table, "Email:", audit.getEmail());
addTableRow(table, "Téléphone:", audit.getPhone());
addTableRow(table, "Secteur:", audit.getSector());
addTableRow(table, "Employés:", audit.getEmployeeCount() + " personnes");
addTableRow(table, "CA annuel:", audit.getTurnover());
addTableRow(table, "Date audit:", audit.getSubmittedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")));
document.add(table);
}
private void addExecutiveSummary(Document document, AuditResponse audit) throws DocumentException {
Paragraph section = new Paragraph("RÉSUMÉ EXÉCUTIF", HEADER_FONT);
section.setSpacingBefore(15);
section.setSpacingAfter(10);
document.add(section);
// Score global avec couleur
String maturityLevel = getMaturityLevel(audit.getMaturityPercentage());
BaseColor scoreColor = getScoreColor(audit.getMaturityPercentage());
Paragraph scoreText = new Paragraph();
scoreText.add(new Chunk("Score de maturité digitale: ", NORMAL_FONT));
scoreText.add(new Chunk(String.format("%.1f%% (%s)", audit.getMaturityPercentage(), maturityLevel),
new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD, scoreColor)));
scoreText.setSpacingAfter(15);
document.add(scoreText);
// Graphique en barres des scores par catégorie
addCategoryChart(document, audit);
}
private void addCategoryScores(Document document, AuditResponse audit) throws DocumentException {
Paragraph section = new Paragraph("ANALYSE DÉTAILLÉE PAR DOMAINE", HEADER_FONT);
section.setSpacingBefore(15);
section.setSpacingAfter(10);
document.add(section);
PdfPTable table = new PdfPTable(3);
table.setWidthPercentage(100);
table.setWidths(new float[]{3, 1, 2});
// En-têtes
addTableHeader(table, "Domaine");
addTableHeader(table, "Score");
addTableHeader(table, "Niveau");
for (Map.Entry<String, Integer> entry : audit.getCategoryScores().entrySet()) {
String category = getCategoryDisplayName(entry.getKey());
Integer score = entry.getValue();
// Calcul du pourcentage (approximatif)
double percentage = (double) score / 100 * 100; // À ajuster selon le scoring réel
String level = getMaturityLevel(percentage);
addTableRow(table, category, score.toString() + " - " + level);
}
document.add(table);
}
private void addRecommendations(Document document, AuditResponse audit) throws DocumentException {
Paragraph section = new Paragraph("RECOMMANDATIONS PRIORITAIRES", HEADER_FONT);
section.setSpacingBefore(15);
section.setSpacingAfter(10);
document.add(section);
Paragraph recommendations = new Paragraph(audit.getRecommendations(), NORMAL_FONT);
recommendations.setSpacingAfter(10);
document.add(recommendations);
if (audit.getPriorityActions() != null && !audit.getPriorityActions().isEmpty()) {
Paragraph actions = new Paragraph("Actions prioritaires: " + audit.getPriorityActions(), NORMAL_FONT);
actions.setSpacingAfter(15);
document.add(actions);
}
}
private void addBudgetEstimation(Document document, AuditResponse audit) throws DocumentException {
if (audit.getEstimatedBudgetMin() != null && audit.getEstimatedBudgetMax() != null) {
Paragraph section = new Paragraph("ESTIMATION BUDGÉTAIRE", HEADER_FONT);
section.setSpacingBefore(15);
section.setSpacingAfter(10);
document.add(section);
String budgetText = String.format("Investissement estimé pour la digitalisation: %,.0f - %,.0f FCFA",
audit.getEstimatedBudgetMin(), audit.getEstimatedBudgetMax());
Paragraph budget = new Paragraph(budgetText, NORMAL_FONT);
budget.setSpacingAfter(10);
document.add(budget);
Paragraph note = new Paragraph("* Estimation basée sur votre niveau de maturité actuel. " +
"Un devis personnalisé sera établi après analyse détaillée.", SMALL_FONT);
note.setSpacingAfter(15);
document.add(note);
}
}
private void addNextSteps(Document document) throws DocumentException {
Paragraph section = new Paragraph("PROCHAINES ÉTAPES", HEADER_FONT);
section.setSpacingBefore(15);
section.setSpacingAfter(10);
document.add(section);
List list = new List(List.ORDERED);
list.add(new ListItem("Rendez-vous diagnostic approfondi (gratuit)", NORMAL_FONT));
list.add(new ListItem("Analyse détaillée de vos processus métier", NORMAL_FONT));
list.add(new ListItem("Proposition de solution personnalisée", NORMAL_FONT));
list.add(new ListItem("Planification du déploiement", NORMAL_FONT));
list.add(new ListItem("Formation de vos équipes", NORMAL_FONT));
document.add(list);
}
private void addFooter(Document document) throws DocumentException {
Paragraph footer = new Paragraph("\nLions Dev - Solutions Digitales Innovantes\n" +
"Abidjan, Côte d'Ivoire | +225 01 01 75 95 25 | contact@lions.dev", SMALL_FONT);
footer.setAlignment(Element.ALIGN_CENTER);
footer.setSpacingBefore(20);
document.add(footer);
}
/**
* Envoie le rapport par email
*/
public void sendAuditReportByEmail(AuditResponse audit, byte[] pdfReport) {
try {
Mail mail = Mail.withHtml(audit.getEmail(),
"Votre Audit de Maturité Digitale - Lions Dev",
generateEmailContent(audit))
.addAttachment("audit-" + audit.getCompanyName() + ".pdf",
pdfReport, "application/pdf");
mailer.send(mail);
} catch (Exception e) {
// Log l'erreur mais ne fait pas échouer le processus
System.err.println("Erreur envoi email: " + e.getMessage());
}
}
private String generateEmailContent(AuditResponse audit) {
return String.format("""
<h2>Bonjour %s,</h2>
<p>Merci d'avoir réalisé l'audit de maturité digitale avec Lions Dev.</p>
<p><strong>Votre score global: %.1f%%</strong></p>
<p>Vous trouverez en pièce jointe votre rapport détaillé avec nos recommandations personnalisées.</p>
<p>Notre équipe vous contactera dans les 24h pour planifier un rendez-vous diagnostic gratuit.</p>
<p>Cordialement,<br>
L'équipe Lions Dev<br>
+225 01 01 75 95 25</p>
""", audit.getContactName(), audit.getMaturityPercentage());
}
// Méthodes utilitaires
private void addTableRow(PdfPTable table, String label, String value) {
table.addCell(new PdfPCell(new Phrase(label, NORMAL_FONT)));
table.addCell(new PdfPCell(new Phrase(value != null ? value : "-", NORMAL_FONT)));
}
private void addTableHeader(PdfPTable table, String header) {
PdfPCell cell = new PdfPCell(new Phrase(header, HEADER_FONT));
cell.setBackgroundColor(BaseColor.LIGHT_GRAY);
table.addCell(cell);
}
private String getMaturityLevel(double percentage) {
if (percentage < 30) return "Débutant";
if (percentage < 60) return "Intermédiaire";
if (percentage < 80) return "Avancé";
return "Expert";
}
private BaseColor getScoreColor(double percentage) {
if (percentage < 30) return BaseColor.RED;
if (percentage < 60) return BaseColor.ORANGE;
if (percentage < 80) return BaseColor.BLUE;
return BaseColor.GREEN;
}
private String getCategoryDisplayName(String category) {
return switch (category) {
case "commercial" -> "Gestion Commerciale";
case "stock" -> "Gestion des Stocks";
case "comptabilite" -> "Comptabilité";
case "rh" -> "Ressources Humaines";
case "infrastructure" -> "Infrastructure IT";
default -> category;
};
}
private void addCategoryChart(Document document, AuditResponse audit) throws DocumentException {
// Graphique simple en texte (pour une vraie implémentation, utiliser JFreeChart)
Paragraph chart = new Paragraph("Répartition des scores par domaine:", NORMAL_FONT);
chart.setSpacingAfter(5);
document.add(chart);
for (Map.Entry<String, Integer> entry : audit.getCategoryScores().entrySet()) {
String category = getCategoryDisplayName(entry.getKey());
Integer score = entry.getValue();
String bar = "".repeat(Math.max(1, score / 10)) + " " + score + "%";
Paragraph barChart = new Paragraph(category + ": " + bar, SMALL_FONT);
document.add(barChart);
}
document.add(Chunk.NEWLINE);
}
}

View File

@@ -0,0 +1,198 @@
package dev.lions.audit;
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;
/**
* API REST pour l'outil d'audit de maturité digitale
*/
@Path("/api/audit")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AuditResource {
@Inject
AuditService auditService;
@Inject
AuditReportService reportService;
/**
* Récupère toutes les questions d'audit par catégorie
*/
@GET
@Path("/questions")
public Response getQuestions(@QueryParam("lang") @DefaultValue("fr") String language) {
try {
Map<String, List<AuditQuestion>> questions = auditService.getQuestionsByCategory();
return Response.ok(questions).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du chargement des questions"))
.build();
}
}
/**
* Soumet les réponses d'audit et génère le rapport
*/
@POST
@Path("/submit")
public Response submitAudit(AuditSubmissionDTO submission) {
try {
// Validation des données
if (submission.getCompanyName() == null || submission.getCompanyName().trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Le nom de l'entreprise est requis"))
.build();
}
if (submission.getEmail() == null || !isValidEmail(submission.getEmail())) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Email valide requis"))
.build();
}
// Création de la réponse d'audit
AuditResponse response = new AuditResponse();
response.setCompanyName(submission.getCompanyName());
response.setContactName(submission.getContactName());
response.setEmail(submission.getEmail());
response.setPhone(submission.getPhone());
response.setSector(submission.getSector());
response.setEmployeeCount(submission.getEmployeeCount());
response.setTurnover(submission.getTurnover());
response.setAnswers(submission.getAnswers());
// Traitement de l'audit
AuditResponse processedResponse = auditService.processAuditResponse(response);
// Génération du rapport PDF
byte[] pdfReport = reportService.generateAuditReport(processedResponse);
// Envoi par email
reportService.sendAuditReportByEmail(processedResponse, pdfReport);
// Réponse avec résumé
AuditResultDTO result = new AuditResultDTO();
result.setAuditId(processedResponse.getId());
result.setMaturityPercentage(processedResponse.getMaturityPercentage());
result.setCategoryScores(processedResponse.getCategoryScores());
result.setRecommendations(processedResponse.getRecommendations());
result.setPriorityActions(processedResponse.getPriorityActions());
result.setEstimatedBudgetMin(processedResponse.getEstimatedBudgetMin());
result.setEstimatedBudgetMax(processedResponse.getEstimatedBudgetMax());
return Response.ok(result).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du traitement de l'audit: " + e.getMessage()))
.build();
}
}
/**
* Télécharge le rapport PDF d'un audit
*/
@GET
@Path("/report/{auditId}")
@Produces("application/pdf")
public Response downloadReport(@PathParam("auditId") Long auditId) {
try {
AuditResponse audit = auditService.getAuditById(auditId);
if (audit == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity("Audit non trouvé")
.build();
}
byte[] pdfReport = reportService.generateAuditReport(audit);
return Response.ok(pdfReport)
.header("Content-Disposition",
"attachment; filename=\"audit-" + audit.getCompanyName() + ".pdf\"")
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la génération du rapport")
.build();
}
}
/**
* Récupère les statistiques d'audit pour le dashboard admin
*/
@GET
@Path("/stats")
public Response getAuditStats() {
try {
Map<String, Object> stats = auditService.getAuditStatistics();
return Response.ok(stats).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du chargement des statistiques"))
.build();
}
}
/**
* Récupère les audits non contactés (pour équipe commerciale)
*/
@GET
@Path("/leads")
public Response getUncontactedLeads() {
try {
List<AuditResponse> leads = auditService.getUncontactedAudits();
return Response.ok(leads).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du chargement des leads"))
.build();
}
}
/**
* Marque un audit comme contacté
*/
@PUT
@Path("/contact/{auditId}")
public Response markAsContacted(@PathParam("auditId") Long auditId,
Map<String, String> notes) {
try {
auditService.markAsContacted(auditId, notes.get("notes"));
return Response.ok(Map.of("success", true)).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la mise à jour"))
.build();
}
}
/**
* Demande de rendez-vous après audit
*/
@POST
@Path("/request-meeting")
public Response requestMeeting(MeetingRequestDTO request) {
try {
auditService.processMeetingRequest(request);
return Response.ok(Map.of("success", true,
"message", "Demande de rendez-vous enregistrée"))
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de l'enregistrement"))
.build();
}
}
private boolean isValidEmail(String email) {
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
}

View File

@@ -0,0 +1,145 @@
package dev.lions.audit;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.Map;
/**
* Réponse d'audit d'une PME avec scoring et recommandations
*/
@Entity
@Table(name = "audit_responses")
public class AuditResponse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Informations entreprise
@Column(nullable = false)
private String companyName;
@Column(nullable = false)
private String contactName;
@Column(nullable = false)
private String email;
private String phone;
private String sector; // Secteur d'activité
private Integer employeeCount;
private String turnover; // Chiffre d'affaires
// Audit
@ElementCollection
@CollectionTable(name = "audit_answers")
@MapKeyColumn(name = "question_id")
@Column(name = "answer_index")
private Map<Long, Integer> answers; // questionId -> index de réponse
@Column(nullable = false)
private LocalDateTime submittedAt;
// Scoring
private Integer totalScore;
private Integer maxPossibleScore;
private Double maturityPercentage;
@ElementCollection
@CollectionTable(name = "audit_category_scores")
@MapKeyColumn(name = "category")
@Column(name = "score")
private Map<String, Integer> categoryScores;
// Recommandations
@Column(length = 2000)
private String recommendations;
@Column(length = 1000)
private String priorityActions;
// Estimation budgétaire
private Double estimatedBudgetMin;
private Double estimatedBudgetMax;
// Suivi commercial
private Boolean contacted = false;
private LocalDateTime contactedAt;
private String salesNotes;
// Constructeurs
public AuditResponse() {
this.submittedAt = LocalDateTime.now();
}
public AuditResponse(String companyName, String contactName, String email) {
this();
this.companyName = companyName;
this.contactName = contactName;
this.email = email;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCompanyName() { return companyName; }
public void setCompanyName(String companyName) { this.companyName = companyName; }
public String getContactName() { return contactName; }
public void setContactName(String contactName) { this.contactName = contactName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getSector() { return sector; }
public void setSector(String sector) { this.sector = sector; }
public Integer getEmployeeCount() { return employeeCount; }
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
public String getTurnover() { return turnover; }
public void setTurnover(String turnover) { this.turnover = turnover; }
public Map<Long, Integer> getAnswers() { return answers; }
public void setAnswers(Map<Long, Integer> answers) { this.answers = answers; }
public LocalDateTime getSubmittedAt() { return submittedAt; }
public void setSubmittedAt(LocalDateTime submittedAt) { this.submittedAt = submittedAt; }
public Integer getTotalScore() { return totalScore; }
public void setTotalScore(Integer totalScore) { this.totalScore = totalScore; }
public Integer getMaxPossibleScore() { return maxPossibleScore; }
public void setMaxPossibleScore(Integer maxPossibleScore) { this.maxPossibleScore = maxPossibleScore; }
public Double getMaturityPercentage() { return maturityPercentage; }
public void setMaturityPercentage(Double maturityPercentage) { this.maturityPercentage = maturityPercentage; }
public Map<String, Integer> getCategoryScores() { return categoryScores; }
public void setCategoryScores(Map<String, Integer> categoryScores) { this.categoryScores = categoryScores; }
public String getRecommendations() { return recommendations; }
public void setRecommendations(String recommendations) { this.recommendations = recommendations; }
public String getPriorityActions() { return priorityActions; }
public void setPriorityActions(String priorityActions) { this.priorityActions = priorityActions; }
public Double getEstimatedBudgetMin() { return estimatedBudgetMin; }
public void setEstimatedBudgetMin(Double estimatedBudgetMin) { this.estimatedBudgetMin = estimatedBudgetMin; }
public Double getEstimatedBudgetMax() { return estimatedBudgetMax; }
public void setEstimatedBudgetMax(Double estimatedBudgetMax) { this.estimatedBudgetMax = estimatedBudgetMax; }
public Boolean getContacted() { return contacted; }
public void setContacted(Boolean contacted) { this.contacted = contacted; }
public LocalDateTime getContactedAt() { return contactedAt; }
public void setContactedAt(LocalDateTime contactedAt) { this.contactedAt = contactedAt; }
public String getSalesNotes() { return salesNotes; }
public void setSalesNotes(String salesNotes) { this.salesNotes = salesNotes; }
}

View File

@@ -0,0 +1,247 @@
package dev.lions.audit;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service de gestion des audits de maturité digitale
*/
@ApplicationScoped
public class AuditService {
@Inject
EntityManager em;
/**
* Récupère toutes les questions d'audit actives par catégorie
*/
public Map<String, List<AuditQuestion>> getQuestionsByCategory() {
List<AuditQuestion> questions = em.createQuery(
"SELECT q FROM AuditQuestion q WHERE q.active = true ORDER BY q.displayOrder",
AuditQuestion.class
).getResultList();
return questions.stream()
.collect(Collectors.groupingBy(AuditQuestion::getCategory));
}
/**
* Calcule le score d'un audit et génère les recommandations
*/
@Transactional
public AuditResponse processAuditResponse(AuditResponse response) {
Map<String, List<AuditQuestion>> questionsByCategory = getQuestionsByCategory();
Map<String, Integer> categoryScores = new HashMap<>();
Map<String, Integer> categoryMaxScores = new HashMap<>();
int totalScore = 0;
int maxPossibleScore = 0;
// Calcul des scores par catégorie
for (Map.Entry<String, List<AuditQuestion>> entry : questionsByCategory.entrySet()) {
String category = entry.getKey();
List<AuditQuestion> questions = entry.getValue();
int categoryScore = 0;
int categoryMaxScore = 0;
for (AuditQuestion question : questions) {
Integer answerIndex = response.getAnswers().get(question.getId());
if (answerIndex != null && answerIndex < question.getScores().size()) {
int questionScore = question.getScores().get(answerIndex) * question.getWeight();
categoryScore += questionScore;
totalScore += questionScore;
}
int maxQuestionScore = Collections.max(question.getScores()) * question.getWeight();
categoryMaxScore += maxQuestionScore;
maxPossibleScore += maxQuestionScore;
}
categoryScores.put(category, categoryScore);
categoryMaxScores.put(category, categoryMaxScore);
}
// Mise à jour des scores
response.setTotalScore(totalScore);
response.setMaxPossibleScore(maxPossibleScore);
response.setMaturityPercentage((double) totalScore / maxPossibleScore * 100);
response.setCategoryScores(categoryScores);
// Génération des recommandations
generateRecommendations(response, categoryScores, categoryMaxScores);
// Estimation budgétaire
estimateBudget(response, categoryScores);
// Sauvegarde
em.persist(response);
return response;
}
/**
* Génère les recommandations personnalisées
*/
private void generateRecommendations(AuditResponse response,
Map<String, Integer> categoryScores,
Map<String, Integer> categoryMaxScores) {
StringBuilder recommendations = new StringBuilder();
List<String> priorities = new ArrayList<>();
// Analyse par catégorie
for (Map.Entry<String, Integer> entry : categoryScores.entrySet()) {
String category = entry.getKey();
int score = entry.getValue();
int maxScore = categoryMaxScores.get(category);
double percentage = (double) score / maxScore * 100;
String categoryName = getCategoryDisplayName(category);
if (percentage < 30) {
recommendations.append("🚨 ").append(categoryName).append(" : Niveau critique - Digitalisation urgente nécessaire\n");
priorities.add("Digitaliser " + categoryName.toLowerCase());
} else if (percentage < 60) {
recommendations.append("⚠️ ").append(categoryName).append(" : Niveau faible - Améliorations importantes recommandées\n");
priorities.add("Améliorer " + categoryName.toLowerCase());
} else if (percentage < 80) {
recommendations.append("").append(categoryName).append(" : Niveau correct - Optimisations possibles\n");
} else {
recommendations.append("🏆 ").append(categoryName).append(" : Excellent niveau de digitalisation\n");
}
}
response.setRecommendations(recommendations.toString());
response.setPriorityActions(String.join(", ", priorities));
}
/**
* Estime le budget nécessaire selon les scores
*/
private void estimateBudget(AuditResponse response, Map<String, Integer> categoryScores) {
double budgetMin = 0;
double budgetMax = 0;
// Tarification par module selon le niveau de maturité
Map<String, Double[]> modulePricing = Map.of(
"commercial", new Double[]{150000.0, 300000.0}, // CRM
"stock", new Double[]{100000.0, 250000.0}, // Gestion stocks
"comptabilite", new Double[]{200000.0, 400000.0}, // Comptabilité
"rh", new Double[]{80000.0, 200000.0}, // RH
"infrastructure", new Double[]{100000.0, 300000.0} // IT
);
for (Map.Entry<String, Integer> entry : categoryScores.entrySet()) {
String category = entry.getKey();
int score = entry.getValue();
if (score < 50 && modulePricing.containsKey(category)) { // Besoin d'amélioration
Double[] pricing = modulePricing.get(category);
budgetMin += pricing[0];
budgetMax += pricing[1];
}
}
response.setEstimatedBudgetMin(budgetMin);
response.setEstimatedBudgetMax(budgetMax);
}
/**
* Récupère les audits non contactés pour le suivi commercial
*/
public List<AuditResponse> getUncontactedAudits() {
return em.createQuery(
"SELECT a FROM AuditResponse a WHERE a.contacted = false ORDER BY a.submittedAt DESC",
AuditResponse.class
).getResultList();
}
/**
* Marque un audit comme contacté
*/
@Transactional
public void markAsContacted(Long auditId, String notes) {
AuditResponse audit = em.find(AuditResponse.class, auditId);
if (audit != null) {
audit.setContacted(true);
audit.setContactedAt(java.time.LocalDateTime.now());
audit.setSalesNotes(notes);
}
}
/**
* Récupère un audit par ID
*/
public AuditResponse getAuditById(Long auditId) {
return em.find(AuditResponse.class, auditId);
}
/**
* Récupère les statistiques d'audit
*/
public Map<String, Object> getAuditStatistics() {
Map<String, Object> stats = new HashMap<>();
// Nombre total d'audits
Long totalAudits = em.createQuery("SELECT COUNT(a) FROM AuditResponse a", Long.class)
.getSingleResult();
stats.put("totalAudits", totalAudits);
// Audits cette semaine
Long weeklyAudits = em.createQuery(
"SELECT COUNT(a) FROM AuditResponse a WHERE a.submittedAt >= :weekStart", Long.class)
.setParameter("weekStart", java.time.LocalDateTime.now().minusDays(7))
.getSingleResult();
stats.put("weeklyAudits", weeklyAudits);
// Score moyen
Double avgScore = em.createQuery(
"SELECT AVG(a.maturityPercentage) FROM AuditResponse a", Double.class)
.getSingleResult();
stats.put("averageScore", avgScore != null ? avgScore : 0.0);
// Répartition par secteur
List<Object[]> sectorStats = em.createQuery(
"SELECT a.sector, COUNT(a) FROM AuditResponse a GROUP BY a.sector", Object[].class)
.getResultList();
Map<String, Long> sectorDistribution = new HashMap<>();
for (Object[] row : sectorStats) {
sectorDistribution.put((String) row[0], (Long) row[1]);
}
stats.put("sectorDistribution", sectorDistribution);
return stats;
}
/**
* Traite une demande de rendez-vous
*/
@Transactional
public void processMeetingRequest(MeetingRequestDTO request) {
// Ici on pourrait créer une entité MeetingRequest
// Pour l'instant, on met à jour les notes de l'audit
AuditResponse audit = em.find(AuditResponse.class, request.getAuditId());
if (audit != null) {
String meetingNote = String.format("RDV demandé: %s à %s (%s) - %s",
request.getPreferredDate(), request.getPreferredTime(),
request.getMeetingType(), request.getMessage());
audit.setSalesNotes(meetingNote);
}
}
private String getCategoryDisplayName(String category) {
return switch (category) {
case "commercial" -> "Gestion Commerciale";
case "stock" -> "Gestion des Stocks";
case "comptabilite" -> "Comptabilité";
case "rh" -> "Ressources Humaines";
case "infrastructure" -> "Infrastructure IT";
default -> category;
};
}
}

View File

@@ -0,0 +1,129 @@
package dev.lions.audit;
import java.util.Map;
/**
* DTO pour la soumission d'un audit
*/
public class AuditSubmissionDTO {
// Informations entreprise
private String companyName;
private String contactName;
private String email;
private String phone;
private String sector;
private Integer employeeCount;
private String turnover;
// Réponses aux questions
private Map<Long, Integer> answers; // questionId -> answerIndex
// Préférences
private String preferredContactTime;
private String additionalComments;
private Boolean acceptsMarketing = false;
// Constructeurs
public AuditSubmissionDTO() {}
// Getters et Setters
public String getCompanyName() { return companyName; }
public void setCompanyName(String companyName) { this.companyName = companyName; }
public String getContactName() { return contactName; }
public void setContactName(String contactName) { this.contactName = contactName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getSector() { return sector; }
public void setSector(String sector) { this.sector = sector; }
public Integer getEmployeeCount() { return employeeCount; }
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
public String getTurnover() { return turnover; }
public void setTurnover(String turnover) { this.turnover = turnover; }
public Map<Long, Integer> getAnswers() { return answers; }
public void setAnswers(Map<Long, Integer> answers) { this.answers = answers; }
public String getPreferredContactTime() { return preferredContactTime; }
public void setPreferredContactTime(String preferredContactTime) { this.preferredContactTime = preferredContactTime; }
public String getAdditionalComments() { return additionalComments; }
public void setAdditionalComments(String additionalComments) { this.additionalComments = additionalComments; }
public Boolean getAcceptsMarketing() { return acceptsMarketing; }
public void setAcceptsMarketing(Boolean acceptsMarketing) { this.acceptsMarketing = acceptsMarketing; }
}
/**
* DTO pour le résultat d'audit
*/
class AuditResultDTO {
private Long auditId;
private Double maturityPercentage;
private Map<String, Integer> categoryScores;
private String recommendations;
private String priorityActions;
private Double estimatedBudgetMin;
private Double estimatedBudgetMax;
private String nextSteps;
// Getters et Setters
public Long getAuditId() { return auditId; }
public void setAuditId(Long auditId) { this.auditId = auditId; }
public Double getMaturityPercentage() { return maturityPercentage; }
public void setMaturityPercentage(Double maturityPercentage) { this.maturityPercentage = maturityPercentage; }
public Map<String, Integer> getCategoryScores() { return categoryScores; }
public void setCategoryScores(Map<String, Integer> categoryScores) { this.categoryScores = categoryScores; }
public String getRecommendations() { return recommendations; }
public void setRecommendations(String recommendations) { this.recommendations = recommendations; }
public String getPriorityActions() { return priorityActions; }
public void setPriorityActions(String priorityActions) { this.priorityActions = priorityActions; }
public Double getEstimatedBudgetMin() { return estimatedBudgetMin; }
public void setEstimatedBudgetMin(Double estimatedBudgetMin) { this.estimatedBudgetMin = estimatedBudgetMin; }
public Double getEstimatedBudgetMax() { return estimatedBudgetMax; }
public void setEstimatedBudgetMax(Double estimatedBudgetMax) { this.estimatedBudgetMax = estimatedBudgetMax; }
public String getNextSteps() { return nextSteps; }
public void setNextSteps(String nextSteps) { this.nextSteps = nextSteps; }
}
/**
* DTO pour demande de rendez-vous
*/
class MeetingRequestDTO {
private Long auditId;
private String preferredDate;
private String preferredTime;
private String meetingType; // "phone", "video", "onsite"
private String message;
// Getters et Setters
public Long getAuditId() { return auditId; }
public void setAuditId(Long auditId) { this.auditId = auditId; }
public String getPreferredDate() { return preferredDate; }
public void setPreferredDate(String preferredDate) { this.preferredDate = preferredDate; }
public String getPreferredTime() { return preferredTime; }
public void setPreferredTime(String preferredTime) { this.preferredTime = preferredTime; }
public String getMeetingType() { return meetingType; }
public void setMeetingType(String meetingType) { this.meetingType = meetingType; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
}

View File

@@ -0,0 +1,534 @@
package dev.lions.compliance;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
/**
* Service de conformité fiscale ivoirienne
*/
@ApplicationScoped
public class IvorianTaxService {
// Taux de TVA en Côte d'Ivoire
public static final double TVA_RATE = 18.0; // 18%
public static final double TVA_REDUCED_RATE = 9.0; // 9% pour certains produits
// Taux d'impôt sur les sociétés
public static final double IS_RATE = 25.0; // 25%
public static final double IS_REDUCED_RATE = 20.0; // 20% pour PME
// Seuils PME
public static final double PME_TURNOVER_THRESHOLD = 200_000_000; // 200M FCFA
/**
* Calcule la TVA sur un montant
*/
public TaxCalculation calculateTVA(double amountHT, boolean reducedRate) {
double rate = reducedRate ? TVA_REDUCED_RATE : TVA_RATE;
double tvaAmount = amountHT * (rate / 100);
double amountTTC = amountHT + tvaAmount;
TaxCalculation calculation = new TaxCalculation();
calculation.setAmountHT(amountHT);
calculation.setTaxRate(rate);
calculation.setTaxAmount(tvaAmount);
calculation.setAmountTTC(amountTTC);
calculation.setTaxType("TVA");
return calculation;
}
/**
* Calcule l'impôt sur les sociétés
*/
public TaxCalculation calculateIS(double annualProfit, boolean isPME) {
double rate = isPME ? IS_REDUCED_RATE : IS_RATE;
double isAmount = annualProfit * (rate / 100);
TaxCalculation calculation = new TaxCalculation();
calculation.setAmountHT(annualProfit);
calculation.setTaxRate(rate);
calculation.setTaxAmount(isAmount);
calculation.setAmountTTC(annualProfit - isAmount); // Bénéfice net après IS
calculation.setTaxType("IS");
return calculation;
}
/**
* Génère la déclaration TVA mensuelle
*/
public TVADeclaration generateTVADeclaration(String tenantId, YearMonth period,
List<TVATransaction> transactions) {
TVADeclaration declaration = new TVADeclaration();
declaration.setTenantId(tenantId);
declaration.setPeriod(period);
declaration.setDeclarationType("MENSUELLE");
double totalVentesHT = 0;
double totalTVACollectee = 0;
double totalAchatsHT = 0;
double totalTVADeductible = 0;
for (TVATransaction transaction : transactions) {
if (transaction.getType() == TransactionType.VENTE) {
totalVentesHT += transaction.getAmountHT();
totalTVACollectee += transaction.getTvaAmount();
} else if (transaction.getType() == TransactionType.ACHAT) {
totalAchatsHT += transaction.getAmountHT();
totalTVADeductible += transaction.getTvaAmount();
}
}
double tvaAVerser = totalTVACollectee - totalTVADeductible;
declaration.setVentesHT(totalVentesHT);
declaration.setTvaCollectee(totalTVACollectee);
declaration.setAchatsHT(totalAchatsHT);
declaration.setTvaDeductible(totalTVADeductible);
declaration.setTvaAVerser(Math.max(0, tvaAVerser));
declaration.setCreditTVA(Math.max(0, -tvaAVerser));
return declaration;
}
/**
* Génère la déclaration IS annuelle
*/
public ISDeclaration generateISDeclaration(String tenantId, int year,
double chiffreAffaires, double charges,
double amortissements) {
ISDeclaration declaration = new ISDeclaration();
declaration.setTenantId(tenantId);
declaration.setYear(year);
double beneficeBrut = chiffreAffaires - charges;
double beneficeImposable = beneficeBrut - amortissements;
boolean isPME = chiffreAffaires <= PME_TURNOVER_THRESHOLD;
TaxCalculation isCalculation = calculateIS(beneficeImposable, isPME);
declaration.setChiffreAffaires(chiffreAffaires);
declaration.setCharges(charges);
declaration.setAmortissements(amortissements);
declaration.setBeneficeBrut(beneficeBrut);
declaration.setBeneficeImposable(beneficeImposable);
declaration.setTauxIS(isCalculation.getTaxRate());
declaration.setMontantIS(isCalculation.getTaxAmount());
declaration.setBeneficeNet(isCalculation.getAmountTTC());
declaration.setIsPME(isPME);
return declaration;
}
/**
* Calcule les cotisations CNPS
*/
public CNPSCalculation calculateCNPS(double salaireBrut) {
CNPSCalculation calculation = new CNPSCalculation();
// Plafond CNPS (à ajuster selon les barèmes en vigueur)
double plafondCNPS = 1_800_000; // 1.8M FCFA par an
double salaireImposable = Math.min(salaireBrut, plafondCNPS);
// Taux CNPS
double tauxEmployeur = 16.75; // 16.75%
double tauxEmploye = 6.3; // 6.3%
double cotisationEmployeur = salaireImposable * (tauxEmployeur / 100);
double cotisationEmploye = salaireImposable * (tauxEmploye / 100);
double cotisationTotale = cotisationEmployeur + cotisationEmploye;
calculation.setSalaireBrut(salaireBrut);
calculation.setSalaireImposable(salaireImposable);
calculation.setTauxEmployeur(tauxEmployeur);
calculation.setTauxEmploye(tauxEmploye);
calculation.setCotisationEmployeur(cotisationEmployeur);
calculation.setCotisationEmploye(cotisationEmploye);
calculation.setCotisationTotale(cotisationTotale);
calculation.setSalaireNet(salaireBrut - cotisationEmploye);
return calculation;
}
/**
* Vérifie la conformité d'une entreprise
*/
public ComplianceReport checkCompliance(String tenantId) {
ComplianceReport report = new ComplianceReport();
report.setTenantId(tenantId);
report.setCheckDate(LocalDate.now());
List<ComplianceIssue> issues = new ArrayList<>();
// Vérifications de base
if (!hasValidTaxNumber(tenantId)) {
issues.add(new ComplianceIssue("TAX_NUMBER", "Numéro contribuable DGI manquant", "HIGH"));
}
if (!hasValidCNPSNumber(tenantId)) {
issues.add(new ComplianceIssue("CNPS_NUMBER", "Numéro CNPS manquant", "HIGH"));
}
if (!hasValidRCCM(tenantId)) {
issues.add(new ComplianceIssue("RCCM", "Numéro RCCM manquant", "MEDIUM"));
}
// Vérifications déclarations
if (!hasRecentTVADeclaration(tenantId)) {
issues.add(new ComplianceIssue("TVA_DECLARATION", "Déclaration TVA en retard", "HIGH"));
}
if (!hasRecentISDeclaration(tenantId)) {
issues.add(new ComplianceIssue("IS_DECLARATION", "Déclaration IS en retard", "HIGH"));
}
report.setIssues(issues);
report.setComplianceScore(calculateComplianceScore(issues));
report.setStatus(issues.isEmpty() ? "COMPLIANT" : "NON_COMPLIANT");
return report;
}
/**
* Génère les échéances fiscales
*/
public List<TaxDeadline> getTaxDeadlines(int year) {
List<TaxDeadline> deadlines = new ArrayList<>();
// Déclarations TVA mensuelles (15 de chaque mois)
for (int month = 1; month <= 12; month++) {
deadlines.add(new TaxDeadline(
"TVA_MENSUELLE",
"Déclaration TVA " + getMonthName(month),
LocalDate.of(year, month, 15),
"HIGH"
));
}
// Déclaration IS annuelle (30 avril)
deadlines.add(new TaxDeadline(
"IS_ANNUELLE",
"Déclaration Impôt sur les Sociétés " + year,
LocalDate.of(year + 1, 4, 30),
"HIGH"
));
// Déclarations CNPS trimestrielles
deadlines.add(new TaxDeadline("CNPS_T1", "Déclaration CNPS T1", LocalDate.of(year, 4, 15), "MEDIUM"));
deadlines.add(new TaxDeadline("CNPS_T2", "Déclaration CNPS T2", LocalDate.of(year, 7, 15), "MEDIUM"));
deadlines.add(new TaxDeadline("CNPS_T3", "Déclaration CNPS T3", LocalDate.of(year, 10, 15), "MEDIUM"));
deadlines.add(new TaxDeadline("CNPS_T4", "Déclaration CNPS T4", LocalDate.of(year + 1, 1, 15), "MEDIUM"));
return deadlines;
}
// Méthodes utilitaires privées
private boolean hasValidTaxNumber(String tenantId) {
// Simulation - en réalité, vérifier en base
return true;
}
private boolean hasValidCNPSNumber(String tenantId) {
// Simulation - en réalité, vérifier en base
return true;
}
private boolean hasValidRCCM(String tenantId) {
// Simulation - en réalité, vérifier en base
return true;
}
private boolean hasRecentTVADeclaration(String tenantId) {
// Simulation - en réalité, vérifier les déclarations récentes
return true;
}
private boolean hasRecentISDeclaration(String tenantId) {
// Simulation - en réalité, vérifier les déclarations récentes
return true;
}
private double calculateComplianceScore(List<ComplianceIssue> issues) {
if (issues.isEmpty()) return 100.0;
double penalty = 0;
for (ComplianceIssue issue : issues) {
switch (issue.getSeverity()) {
case "HIGH" -> penalty += 20;
case "MEDIUM" -> penalty += 10;
case "LOW" -> penalty += 5;
}
}
return Math.max(0, 100 - penalty);
}
private String getMonthName(int month) {
String[] months = {"", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"};
return months[month];
}
}
/**
* Calcul fiscal
*/
class TaxCalculation {
private double amountHT;
private double taxRate;
private double taxAmount;
private double amountTTC;
private String taxType;
// Getters et Setters
public double getAmountHT() { return amountHT; }
public void setAmountHT(double amountHT) { this.amountHT = amountHT; }
public double getTaxRate() { return taxRate; }
public void setTaxRate(double taxRate) { this.taxRate = taxRate; }
public double getTaxAmount() { return taxAmount; }
public void setTaxAmount(double taxAmount) { this.taxAmount = taxAmount; }
public double getAmountTTC() { return amountTTC; }
public void setAmountTTC(double amountTTC) { this.amountTTC = amountTTC; }
public String getTaxType() { return taxType; }
public void setTaxType(String taxType) { this.taxType = taxType; }
}
/**
* Transaction TVA
*/
class TVATransaction {
private TransactionType type;
private double amountHT;
private double tvaAmount;
private LocalDate date;
// Constructeurs et getters/setters
public TransactionType getType() { return type; }
public void setType(TransactionType type) { this.type = type; }
public double getAmountHT() { return amountHT; }
public void setAmountHT(double amountHT) { this.amountHT = amountHT; }
public double getTvaAmount() { return tvaAmount; }
public void setTvaAmount(double tvaAmount) { this.tvaAmount = tvaAmount; }
public LocalDate getDate() { return date; }
public void setDate(LocalDate date) { this.date = date; }
}
enum TransactionType {
VENTE, ACHAT
}
/**
* Déclaration TVA
*/
class TVADeclaration {
private String tenantId;
private YearMonth period;
private String declarationType;
private double ventesHT;
private double tvaCollectee;
private double achatsHT;
private double tvaDeductible;
private double tvaAVerser;
private double creditTVA;
// Getters et Setters complets...
public String getTenantId() { return tenantId; }
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
public YearMonth getPeriod() { return period; }
public void setPeriod(YearMonth period) { this.period = period; }
public String getDeclarationType() { return declarationType; }
public void setDeclarationType(String declarationType) { this.declarationType = declarationType; }
public double getVentesHT() { return ventesHT; }
public void setVentesHT(double ventesHT) { this.ventesHT = ventesHT; }
public double getTvaCollectee() { return tvaCollectee; }
public void setTvaCollectee(double tvaCollectee) { this.tvaCollectee = tvaCollectee; }
public double getAchatsHT() { return achatsHT; }
public void setAchatsHT(double achatsHT) { this.achatsHT = achatsHT; }
public double getTvaDeductible() { return tvaDeductible; }
public void setTvaDeductible(double tvaDeductible) { this.tvaDeductible = tvaDeductible; }
public double getTvaAVerser() { return tvaAVerser; }
public void setTvaAVerser(double tvaAVerser) { this.tvaAVerser = tvaAVerser; }
public double getCreditTVA() { return creditTVA; }
public void setCreditTVA(double creditTVA) { this.creditTVA = creditTVA; }
}
/**
* Déclaration IS
*/
class ISDeclaration {
private String tenantId;
private int year;
private double chiffreAffaires;
private double charges;
private double amortissements;
private double beneficeBrut;
private double beneficeImposable;
private double tauxIS;
private double montantIS;
private double beneficeNet;
private boolean isPME;
// Getters et Setters complets...
public String getTenantId() { return tenantId; }
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
public int getYear() { return year; }
public void setYear(int year) { this.year = year; }
public double getChiffreAffaires() { return chiffreAffaires; }
public void setChiffreAffaires(double chiffreAffaires) { this.chiffreAffaires = chiffreAffaires; }
public double getCharges() { return charges; }
public void setCharges(double charges) { this.charges = charges; }
public double getAmortissements() { return amortissements; }
public void setAmortissements(double amortissements) { this.amortissements = amortissements; }
public double getBeneficeBrut() { return beneficeBrut; }
public void setBeneficeBrut(double beneficeBrut) { this.beneficeBrut = beneficeBrut; }
public double getBeneficeImposable() { return beneficeImposable; }
public void setBeneficeImposable(double beneficeImposable) { this.beneficeImposable = beneficeImposable; }
public double getTauxIS() { return tauxIS; }
public void setTauxIS(double tauxIS) { this.tauxIS = tauxIS; }
public double getMontantIS() { return montantIS; }
public void setMontantIS(double montantIS) { this.montantIS = montantIS; }
public double getBeneficeNet() { return beneficeNet; }
public void setBeneficeNet(double beneficeNet) { this.beneficeNet = beneficeNet; }
public boolean getIsPME() { return isPME; }
public void setIsPME(boolean isPME) { this.isPME = isPME; }
}
/**
* Calcul CNPS
*/
class CNPSCalculation {
private double salaireBrut;
private double salaireImposable;
private double tauxEmployeur;
private double tauxEmploye;
private double cotisationEmployeur;
private double cotisationEmploye;
private double cotisationTotale;
private double salaireNet;
// Getters et Setters complets...
public double getSalaireBrut() { return salaireBrut; }
public void setSalaireBrut(double salaireBrut) { this.salaireBrut = salaireBrut; }
public double getSalaireImposable() { return salaireImposable; }
public void setSalaireImposable(double salaireImposable) { this.salaireImposable = salaireImposable; }
public double getTauxEmployeur() { return tauxEmployeur; }
public void setTauxEmployeur(double tauxEmployeur) { this.tauxEmployeur = tauxEmployeur; }
public double getTauxEmploye() { return tauxEmploye; }
public void setTauxEmploye(double tauxEmploye) { this.tauxEmploye = tauxEmploye; }
public double getCotisationEmployeur() { return cotisationEmployeur; }
public void setCotisationEmployeur(double cotisationEmployeur) { this.cotisationEmployeur = cotisationEmployeur; }
public double getCotisationEmploye() { return cotisationEmploye; }
public void setCotisationEmploye(double cotisationEmploye) { this.cotisationEmploye = cotisationEmploye; }
public double getCotisationTotale() { return cotisationTotale; }
public void setCotisationTotale(double cotisationTotale) { this.cotisationTotale = cotisationTotale; }
public double getSalaireNet() { return salaireNet; }
public void setSalaireNet(double salaireNet) { this.salaireNet = salaireNet; }
}
/**
* Rapport de conformité
*/
class ComplianceReport {
private String tenantId;
private LocalDate checkDate;
private List<ComplianceIssue> issues;
private double complianceScore;
private String status;
// Getters et Setters
public String getTenantId() { return tenantId; }
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
public LocalDate getCheckDate() { return checkDate; }
public void setCheckDate(LocalDate checkDate) { this.checkDate = checkDate; }
public List<ComplianceIssue> getIssues() { return issues; }
public void setIssues(List<ComplianceIssue> issues) { this.issues = issues; }
public double getComplianceScore() { return complianceScore; }
public void setComplianceScore(double complianceScore) { this.complianceScore = complianceScore; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
/**
* Problème de conformité
*/
class ComplianceIssue {
private String code;
private String description;
private String severity;
public ComplianceIssue(String code, String description, String severity) {
this.code = code;
this.description = description;
this.severity = severity;
}
// Getters
public String getCode() { return code; }
public String getDescription() { return description; }
public String getSeverity() { return severity; }
}
/**
* Échéance fiscale
*/
class TaxDeadline {
private String type;
private String description;
private LocalDate dueDate;
private String priority;
public TaxDeadline(String type, String description, LocalDate dueDate, String priority) {
this.type = type;
this.description = description;
this.dueDate = dueDate;
this.priority = priority;
}
// Getters
public String getType() { return type; }
public String getDescription() { return description; }
public LocalDate getDueDate() { return dueDate; }
public String getPriority() { return priority; }
}

View File

@@ -1,171 +1,171 @@
package dev.lions.components; package dev.lions.components;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named; import jakarta.inject.Named;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.primefaces.model.charts.*; import org.primefaces.model.charts.*;
import org.primefaces.model.charts.bar.*; import org.primefaces.model.charts.bar.*;
import org.primefaces.model.charts.line.*; import org.primefaces.model.charts.line.*;
import org.primefaces.model.charts.pie.*; import org.primefaces.model.charts.pie.*;
import org.primefaces.model.charts.optionconfig.title.Title; import org.primefaces.model.charts.optionconfig.title.Title;
import org.primefaces.model.charts.optionconfig.legend.Legend; import org.primefaces.model.charts.optionconfig.legend.Legend;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;
/** /**
* Composant de gestion des graphiques. * Composant de gestion des graphiques.
* Fournit des modèles pour les graphiques linéaires, en barres et circulaires. * Fournit des modèles pour les graphiques linéaires, en barres et circulaires.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 2.1 * @version 2.1
*/ */
@Slf4j @Slf4j
@Named @Named
@ViewScoped @ViewScoped
public class ChartComponent implements Serializable { public class ChartComponent implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final List<String> CHART_COLORS = Arrays.asList( private static final List<String> CHART_COLORS = Arrays.asList(
"rgba(33, 150, 243, 0.8)", "rgba(33, 150, 243, 0.8)",
"rgba(255, 64, 129, 0.8)", "rgba(255, 64, 129, 0.8)",
"rgba(255, 193, 7, 0.8)", "rgba(255, 193, 7, 0.8)",
"rgba(76, 175, 80, 0.8)", "rgba(76, 175, 80, 0.8)",
"rgba(156, 39, 176, 0.8)" "rgba(156, 39, 176, 0.8)"
); );
@Getter @Setter @Getter @Setter
private LineChartModel lineModel; private LineChartModel lineModel;
@Getter @Setter @Getter @Setter
private BarChartModel barModel; private BarChartModel barModel;
@Getter @Setter @Getter @Setter
private PieChartModel pieModel; private PieChartModel pieModel;
/** /**
* Initialise les modèles de graphiques lors de la construction du composant. * Initialise les modèles de graphiques lors de la construction du composant.
*/ */
@PostConstruct @PostConstruct
public void init() { public void init() {
log.info("Initialisation des modèles de graphiques."); log.info("Initialisation des modèles de graphiques.");
createLineModel(); createLineModel();
createBarModel(); createBarModel();
createPieModel(); createPieModel();
} }
/** /**
* Crée un modèle de graphique linéaire. * Crée un modèle de graphique linéaire.
*/ */
private void createLineModel() { private void createLineModel() {
lineModel = new LineChartModel(); lineModel = new LineChartModel();
ChartData data = new ChartData(); ChartData data = new ChartData();
LineChartDataSet dataSet = new LineChartDataSet(); LineChartDataSet dataSet = new LineChartDataSet();
dataSet.setLabel("Évolution des ventes"); dataSet.setLabel("Évolution des ventes");
dataSet.setData(new ArrayList<>(generateRandomData(6))); dataSet.setData(new ArrayList<>(generateRandomData(6)));
dataSet.setBorderColor(CHART_COLORS.get(0)); dataSet.setBorderColor(CHART_COLORS.get(0));
dataSet.setFill(false); dataSet.setFill(false);
dataSet.setTension(0.4); dataSet.setTension(0.4);
data.addChartDataSet(dataSet); data.addChartDataSet(dataSet);
data.setLabels(generateLabels(6, "Mois")); data.setLabels(generateLabels(6, "Mois"));
lineModel.setData(data); lineModel.setData(data);
addChartOptions(lineModel, "Évolution temporelle"); addChartOptions(lineModel, "Évolution temporelle");
log.debug("Modèle de graphique linéaire créé."); log.debug("Modèle de graphique linéaire créé.");
} }
/** /**
* Crée un modèle de graphique en barres. * Crée un modèle de graphique en barres.
*/ */
private void createBarModel() { private void createBarModel() {
barModel = new BarChartModel(); barModel = new BarChartModel();
ChartData data = new ChartData(); ChartData data = new ChartData();
BarChartDataSet dataSet = new BarChartDataSet(); BarChartDataSet dataSet = new BarChartDataSet();
dataSet.setLabel("Performance par trimestre"); dataSet.setLabel("Performance par trimestre");
dataSet.setData(new ArrayList<>(generateRandomData(4))); dataSet.setData(new ArrayList<>(generateRandomData(4)));
dataSet.setBackgroundColor(CHART_COLORS.get(1)); dataSet.setBackgroundColor(CHART_COLORS.get(1));
data.addChartDataSet(dataSet); data.addChartDataSet(dataSet);
data.setLabels(generateLabels(4, "T")); data.setLabels(generateLabels(4, "T"));
barModel.setData(data); barModel.setData(data);
addChartOptions(barModel, "Performance trimestrielle"); addChartOptions(barModel, "Performance trimestrielle");
log.debug("Modèle de graphique en barres créé."); log.debug("Modèle de graphique en barres créé.");
} }
/** /**
* Crée un modèle de graphique circulaire. * Crée un modèle de graphique circulaire.
*/ */
private void createPieModel() { private void createPieModel() {
pieModel = new PieChartModel(); pieModel = new PieChartModel();
ChartData data = new ChartData(); ChartData data = new ChartData();
PieChartDataSet dataSet = new PieChartDataSet(); PieChartDataSet dataSet = new PieChartDataSet();
dataSet.setData(Arrays.asList(25, 35, 40)); dataSet.setData(Arrays.asList(25, 35, 40));
dataSet.setBackgroundColor(CHART_COLORS); dataSet.setBackgroundColor(CHART_COLORS);
data.addChartDataSet(dataSet); data.addChartDataSet(dataSet);
data.setLabels(Arrays.asList("Développement", "Marketing", "Infrastructure")); data.setLabels(Arrays.asList("Développement", "Marketing", "Infrastructure"));
pieModel.setData(data); pieModel.setData(data);
addChartOptions(pieModel, "Répartition des activités"); addChartOptions(pieModel, "Répartition des activités");
log.debug("Modèle de graphique circulaire créé."); log.debug("Modèle de graphique circulaire créé.");
} }
/** /**
* Ajoute des options telles que le titre et la légende aux graphiques. * Ajoute des options telles que le titre et la légende aux graphiques.
* *
* @param model Le modèle de graphique. * @param model Le modèle de graphique.
* @param title Titre du graphique. * @param title Titre du graphique.
*/ */
private void addChartOptions(ChartModel model, String title) { private void addChartOptions(ChartModel model, String title) {
Title chartTitle = new Title(); Title chartTitle = new Title();
chartTitle.setDisplay(true); chartTitle.setDisplay(true);
chartTitle.setText(title); chartTitle.setText(title);
Legend legend = new Legend(); Legend legend = new Legend();
legend.setDisplay(true); legend.setDisplay(true);
legend.setPosition("bottom"); legend.setPosition("bottom");
if (model instanceof LineChartModel) { if (model instanceof LineChartModel) {
LineChartModel lineChart = (LineChartModel) model; LineChartModel lineChart = (LineChartModel) model;
lineChart.setExtender((String) chartTitle.getText()); lineChart.setExtender((String) chartTitle.getText());
} else if (model instanceof BarChartModel) { } else if (model instanceof BarChartModel) {
BarChartModel barChart = (BarChartModel) model; BarChartModel barChart = (BarChartModel) model;
barChart.setExtender((String) chartTitle.getText()); barChart.setExtender((String) chartTitle.getText());
} else if (model instanceof PieChartModel) { } else if (model instanceof PieChartModel) {
PieChartModel pieChart = (PieChartModel) model; PieChartModel pieChart = (PieChartModel) model;
pieChart.setExtender((String) chartTitle.getText()); pieChart.setExtender((String) chartTitle.getText());
} }
} }
private List<Number> generateRandomData(int size) { private List<Number> generateRandomData(int size) {
Random random = new Random(); Random random = new Random();
List<Number> data = new ArrayList<>(); List<Number> data = new ArrayList<>();
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
data.add(random.nextInt(100)); data.add(random.nextInt(100));
} }
return data; return data;
} }
private List<String> generateLabels(int size, String prefix) { private List<String> generateLabels(int size, String prefix) {
List<String> labels = new ArrayList<>(); List<String> labels = new ArrayList<>();
for (int i = 1; i <= size; i++) { for (int i = 1; i <= size; i++) {
labels.add(prefix + " " + i); labels.add(prefix + " " + i);
} }
return labels; return labels;
} }
} }

View File

@@ -1,4 +1,4 @@
package dev.lions.components; package dev.lions.components;
public class DataTableView { public class DataTableView {
} }

View File

@@ -1,295 +1,295 @@
package dev.lions.components; package dev.lions.components;
import jakarta.enterprise.context.Dependent; import jakarta.enterprise.context.Dependent;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named; import jakarta.inject.Named;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.io.Serial; import java.io.Serial;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.primefaces.model.FilterMeta; import org.primefaces.model.FilterMeta;
import org.primefaces.model.LazyDataModel; import org.primefaces.model.LazyDataModel;
import org.primefaces.model.SortMeta; import org.primefaces.model.SortMeta;
import org.primefaces.event.data.PageEvent; import org.primefaces.event.data.PageEvent;
import dev.lions.utils.Column; import dev.lions.utils.Column;
import dev.lions.utils.FilterCriteria; import dev.lions.utils.FilterCriteria;
import dev.lions.exceptions.DataTableException; import dev.lions.exceptions.DataTableException;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Composant de tableau de données dynamique avec support de pagination, tri et filtrage. * Composant de tableau de données dynamique avec support de pagination, tri et filtrage.
* Fournit une interface riche et performante pour l'affichage et la manipulation des données * Fournit une interface riche et performante pour l'affichage et la manipulation des données
* tabulaires dans l'application. * tabulaires dans l'application.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 2.1 * @version 2.1
*/ */
@Named @Named
@Dependent @Dependent
@Slf4j @Slf4j
public class DynamicDataTable<T> implements Serializable { public class DynamicDataTable<T> implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final int DEFAULT_PAGE_SIZE = 10; private static final int DEFAULT_PAGE_SIZE = 10;
private static final int MAX_PAGE_SIZE = 100; private static final int MAX_PAGE_SIZE = 100;
private static final String DEFAULT_EMPTY_MESSAGE = "Aucune donnée disponible"; private static final String DEFAULT_EMPTY_MESSAGE = "Aucune donnée disponible";
@Getter @Setter @Getter @Setter
private List<T> data; private List<T> data;
@Getter @Setter @Getter @Setter
private List<Column> columns; private List<Column> columns;
@Getter @Setter @Getter @Setter
private String emptyMessage = DEFAULT_EMPTY_MESSAGE; private String emptyMessage = DEFAULT_EMPTY_MESSAGE;
@Getter @Setter @Getter @Setter
@Min(1) @Min(1)
private int pageSize = DEFAULT_PAGE_SIZE; private int pageSize = DEFAULT_PAGE_SIZE;
@Getter @Getter
private LazyDataModel<T> lazyModel; private LazyDataModel<T> lazyModel;
@Getter @Getter
private final Map<String, FilterCriteria> activeFilters = new ConcurrentHashMap<>(); private final Map<String, FilterCriteria> activeFilters = new ConcurrentHashMap<>();
private final Map<String, Comparator<T>> customSorters = new HashMap<>(); private final Map<String, Comparator<T>> customSorters = new HashMap<>();
private final Map<String, PropertyAccessor<T>> propertyAccessors = new HashMap<>(); private final Map<String, PropertyAccessor<T>> propertyAccessors = new HashMap<>();
/** /**
* Initialise le tableau avec les données et les colonnes spécifiées. * Initialise le tableau avec les données et les colonnes spécifiées.
* *
* @param data Données à afficher * @param data Données à afficher
* @param columns Configuration des colonnes * @param columns Configuration des colonnes
*/ */
public void initialize(@NotNull List<T> data, @NotNull List<Column> columns) { public void initialize(@NotNull List<T> data, @NotNull List<Column> columns) {
log.info("Initialisation du tableau dynamique avec {} enregistrements", data.size()); log.info("Initialisation du tableau dynamique avec {} enregistrements", data.size());
validateInitializationParameters(data, columns); validateInitializationParameters(data, columns);
this.data = new ArrayList<>(data); this.data = new ArrayList<>(data);
this.columns = new ArrayList<>(columns); this.columns = new ArrayList<>(columns);
initializePropertyAccessors(); initializePropertyAccessors();
initializeLazyLoading(); initializeLazyLoading();
} }
/** /**
* Configure le modèle de chargement paresseux des données. * Configure le modèle de chargement paresseux des données.
*/ */
private void initializeLazyLoading() { private void initializeLazyLoading() {
lazyModel = new LazyDataModel<T>() { lazyModel = new LazyDataModel<T>() {
@Override @Override
public List<T> load(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) { public List<T> load(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) {
try { try {
return loadDataPage(first, pageSize, sortBy, filterBy); return loadDataPage(first, pageSize, sortBy, filterBy);
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors du chargement des données", e); log.error("Erreur lors du chargement des données", e);
throw new DataTableException("Échec du chargement des données", e); throw new DataTableException("Échec du chargement des données", e);
} }
} }
@Override @Override
public int count(Map<String, FilterMeta> filterBy) { public int count(Map<String, FilterMeta> filterBy) {
return data == null ? 0 : data.size(); return data == null ? 0 : data.size();
} }
}; };
lazyModel.setRowCount(data.size()); lazyModel.setRowCount(data.size());
} }
/** /**
* Charge une page de données selon les critères spécifiés. * Charge une page de données selon les critères spécifiés.
*/ */
protected List<T> loadDataPage(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) { protected List<T> loadDataPage(int first, int pageSize, Map<String, SortMeta> sortBy, Map<String, FilterMeta> filterBy) {
return data.stream() return data.stream()
.filter(item -> applyFilters(item, filterBy)) .filter(item -> applyFilters(item, filterBy))
.sorted((a, b) -> applySorting(a, b, sortBy)) .sorted((a, b) -> applySorting(a, b, sortBy))
.skip(first) .skip(first)
.limit(pageSize) .limit(pageSize)
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/** /**
* Applique les filtres sur un élément. * Applique les filtres sur un élément.
*/ */
private boolean applyFilters(T item, Map<String, FilterMeta> filterBy) { private boolean applyFilters(T item, Map<String, FilterMeta> filterBy) {
if (filterBy == null || filterBy.isEmpty()) return true; if (filterBy == null || filterBy.isEmpty()) return true;
return filterBy.entrySet().stream().allMatch(entry -> { return filterBy.entrySet().stream().allMatch(entry -> {
Object filterValue = entry.getValue().getFilterValue(); Object filterValue = entry.getValue().getFilterValue();
if (filterValue == null) return true; if (filterValue == null) return true;
try { try {
Object value = getPropertyValue(item, entry.getKey()); Object value = getPropertyValue(item, entry.getKey());
return value != null && value.toString().toLowerCase().contains(filterValue.toString().toLowerCase()); return value != null && value.toString().toLowerCase().contains(filterValue.toString().toLowerCase());
} catch (Exception e) { } catch (Exception e) {
log.warn("Erreur lors du filtrage", e); log.warn("Erreur lors du filtrage", e);
return false; return false;
} }
}); });
} }
/** /**
* Vérifie si un élément correspond à un critère de filtrage. * Vérifie si un élément correspond à un critère de filtrage.
*/ */
private boolean matchesFilter(T item, String property, Object filterValue) { private boolean matchesFilter(T item, String property, Object filterValue) {
if (filterValue == null) { if (filterValue == null) {
return true; return true;
} }
try { try {
Object value = getPropertyValue(item, property); Object value = getPropertyValue(item, property);
return compareValues(value, filterValue); return compareValues(value, filterValue);
} catch (Exception e) { } catch (Exception e) {
log.warn("Erreur lors du filtrage de la propriété: {}", property, e); log.warn("Erreur lors du filtrage de la propriété: {}", property, e);
return false; return false;
} }
} }
/** /**
* Compare deux valeurs pour le filtrage. * Compare deux valeurs pour le filtrage.
*/ */
private boolean compareValues(Object value, Object filterValue) { private boolean compareValues(Object value, Object filterValue) {
if (value == null) { if (value == null) {
return filterValue == null; return filterValue == null;
} }
String valueStr = value.toString().toLowerCase(); String valueStr = value.toString().toLowerCase();
String filterStr = filterValue.toString().toLowerCase(); String filterStr = filterValue.toString().toLowerCase();
return valueStr.contains(filterStr); return valueStr.contains(filterStr);
} }
/** /**
* Applique le tri sur les données. * Applique le tri sur les données.
*/ */
private int applySorting(T a, T b, Map<String, SortMeta> sortBy) { private int applySorting(T a, T b, Map<String, SortMeta> sortBy) {
for (Map.Entry<String, SortMeta> entry : sortBy.entrySet()) { for (Map.Entry<String, SortMeta> entry : sortBy.entrySet()) {
String property = entry.getKey(); String property = entry.getKey();
try { try {
Comparable valueA = (Comparable) getPropertyValue(a, property); Comparable valueA = (Comparable) getPropertyValue(a, property);
Comparable valueB = (Comparable) getPropertyValue(b, property); Comparable valueB = (Comparable) getPropertyValue(b, property);
int result = valueA.compareTo(valueB); int result = valueA.compareTo(valueB);
return entry.getValue().getOrder().isAscending() ? result : -result; return entry.getValue().getOrder().isAscending() ? result : -result;
} catch (Exception e) { } catch (Exception e) {
log.warn("Erreur lors du tri", e); log.warn("Erreur lors du tri", e);
} }
} }
return 0; return 0;
} }
/** /**
* Compare les valeurs de deux propriétés pour le tri. * Compare les valeurs de deux propriétés pour le tri.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private int compareProperties(T a, T b, String property) { private int compareProperties(T a, T b, String property) {
try { try {
Comparable valueA = (Comparable) getPropertyValue(a, property); Comparable valueA = (Comparable) getPropertyValue(a, property);
Comparable valueB = (Comparable) getPropertyValue(b, property); Comparable valueB = (Comparable) getPropertyValue(b, property);
if (valueA == null && valueB == null) return 0; if (valueA == null && valueB == null) return 0;
if (valueA == null) return -1; if (valueA == null) return -1;
if (valueB == null) return 1; if (valueB == null) return 1;
return valueA.compareTo(valueB); return valueA.compareTo(valueB);
} catch (Exception e) { } catch (Exception e) {
log.warn("Erreur lors de la comparaison de la propriété: {}", property, e); log.warn("Erreur lors de la comparaison de la propriété: {}", property, e);
return 0; return 0;
} }
} }
/** /**
* Initialise les accesseurs de propriétés pour optimiser les performances. * Initialise les accesseurs de propriétés pour optimiser les performances.
*/ */
private void initializePropertyAccessors() { private void initializePropertyAccessors() {
columns.forEach(column -> { columns.forEach(column -> {
String property = column.getField(); String property = column.getField();
try { try {
Method getter = findGetter(property); Method getter = findGetter(property);
propertyAccessors.put(property, item -> getter.invoke(item)); propertyAccessors.put(property, item -> getter.invoke(item));
} catch (Exception e) { } catch (Exception e) {
log.warn("Impossible de créer l'accesseur pour la propriété: {}", property, e); log.warn("Impossible de créer l'accesseur pour la propriété: {}", property, e);
} }
}); });
} }
/** /**
* Trouve la méthode getter pour une propriété. * Trouve la méthode getter pour une propriété.
*/ */
private Method findGetter(String property) throws NoSuchMethodException { private Method findGetter(String property) throws NoSuchMethodException {
String getterName = "get" + property.substring(0, 1).toUpperCase() + property.substring(1); String getterName = "get" + property.substring(0, 1).toUpperCase() + property.substring(1);
return data.get(0).getClass().getMethod(getterName); return data.get(0).getClass().getMethod(getterName);
} }
/** /**
* Interface fonctionnelle pour l'accès aux propriétés. * Interface fonctionnelle pour l'accès aux propriétés.
*/ */
@FunctionalInterface @FunctionalInterface
private interface PropertyAccessor<T> { private interface PropertyAccessor<T> {
Object access(T item) throws Exception; Object access(T item) throws Exception;
} }
/** /**
* Ajoute un trieur personnalisé pour une colonne. * Ajoute un trieur personnalisé pour une colonne.
*/ */
public void addCustomSorter(String property, Comparator<T> comparator) { public void addCustomSorter(String property, Comparator<T> comparator) {
customSorters.put(property, comparator); customSorters.put(property, comparator);
} }
/** /**
* Met à jour le nombre total de lignes. * Met à jour le nombre total de lignes.
*/ */
private void updateRowCount() { private void updateRowCount() {
if (lazyModel != null) { if (lazyModel != null) {
lazyModel.setRowCount(data.size()); lazyModel.setRowCount(data.size());
} }
} }
/** /**
* Gère l'événement de changement de page. * Gère l'événement de changement de page.
*/ */
public void onPageChange(PageEvent event) { public void onPageChange(PageEvent event) {
log.debug("Changement de page: {}", event.getPage()); log.debug("Changement de page: {}", event.getPage());
} }
/** /**
* Valide les paramètres d'initialisation. * Valide les paramètres d'initialisation.
*/ */
private void validateInitializationParameters(List<T> data, List<Column> columns) { private void validateInitializationParameters(List<T> data, List<Column> columns) {
if (data == null || data.isEmpty()) { if (data == null || data.isEmpty()) {
throw new DataTableException("Les données ne peuvent pas être nulles ou vides"); throw new DataTableException("Les données ne peuvent pas être nulles ou vides");
} }
if (columns == null || columns.isEmpty()) { if (columns == null || columns.isEmpty()) {
throw new DataTableException("La configuration des colonnes est requise"); throw new DataTableException("La configuration des colonnes est requise");
} }
} }
/** /**
* Rafraîchit les données du tableau. * Rafraîchit les données du tableau.
*/ */
public void refresh() { public void refresh() {
log.debug("Rafraîchissement du tableau"); log.debug("Rafraîchissement du tableau");
updateRowCount(); updateRowCount();
} }
private Object getPropertyValue(T item, String property) throws Exception { private Object getPropertyValue(T item, String property) throws Exception {
PropertyAccessor<T> accessor = propertyAccessors.get(property); PropertyAccessor<T> accessor = propertyAccessors.get(property);
if (accessor != null) { if (accessor != null) {
return accessor.access(item); return accessor.access(item);
} }
throw new NoSuchFieldException("Propriété inaccessible : " + property); throw new NoSuchFieldException("Propriété inaccessible : " + property);
} }
} }

View File

@@ -1,226 +1,226 @@
package dev.lions.components; package dev.lions.components;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import jakarta.faces.application.FacesMessage; import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext; import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.io.Serial; import java.io.Serial;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.primefaces.event.FileUploadEvent; import org.primefaces.event.FileUploadEvent;
import org.primefaces.model.file.UploadedFile; import org.primefaces.model.file.UploadedFile;
import dev.lions.config.ApplicationConfig; import dev.lions.config.ApplicationConfig;
import dev.lions.exceptions.FileUploadException; import dev.lions.exceptions.FileUploadException;
import dev.lions.services.FileStorageService; import dev.lions.services.FileStorageService;
import dev.lions.utils.FileValidator; import dev.lions.utils.FileValidator;
import dev.lions.utils.SecurityUtils; import dev.lions.utils.SecurityUtils;
import java.io.Serializable; import java.io.Serializable;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
/** /**
* Composant de gestion des téléchargements de fichiers. * Composant de gestion des téléchargements de fichiers.
* Fournit une interface sécurisée et performante pour le téléchargement, * Fournit une interface sécurisée et performante pour le téléchargement,
* la validation et la gestion des fichiers dans l'application. * la validation et la gestion des fichiers dans l'application.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 2.1 * @version 2.1
*/ */
@Named @Named
@ViewScoped @ViewScoped
@Slf4j @Slf4j
public class FileUploadComponent implements Serializable { public class FileUploadComponent implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final int MAX_FILES = 10; // Limite de fichiers autorisés private static final int MAX_FILES = 10; // Limite de fichiers autorisés
private static final String TEMP_DIR_PREFIX = "upload_"; // Préfixe pour répertoire temporaire private static final String TEMP_DIR_PREFIX = "upload_"; // Préfixe pour répertoire temporaire
@Inject @Inject
ApplicationConfig appConfig; ApplicationConfig appConfig;
@Inject @Inject
FileStorageService storageService; FileStorageService storageService;
@Inject @Inject
FileValidator fileValidator; FileValidator fileValidator;
@Inject @Inject
SecurityUtils securityUtils; SecurityUtils securityUtils;
@Getter @Getter
private final List<UploadedFileInfo> uploadedFiles = new ArrayList<>(); private final List<UploadedFileInfo> uploadedFiles = new ArrayList<>();
@Getter @Getter
@Setter @Setter
private String uploadDirectory; private String uploadDirectory;
@Getter @Getter
@Setter @Setter
private boolean multiple = false; private boolean multiple = false;
@Getter @Getter
@Setter @Setter
private String acceptedTypes; private String acceptedTypes;
@Getter @Getter
@Setter @Setter
private long maxFileSize; private long maxFileSize;
/** /**
* Initialisation du composant. * Initialisation du composant.
*/ */
@PostConstruct @PostConstruct
public void init() { public void init() {
this.maxFileSize = appConfig.getMaxFileSize(); this.maxFileSize = appConfig.getMaxFileSize();
this.acceptedTypes = appConfig.getAllowedFileTypes(); this.acceptedTypes = appConfig.getAllowedFileTypes();
this.uploadDirectory = createTempUploadDirectory(); this.uploadDirectory = createTempUploadDirectory();
log.info("Composant de téléchargement initialisé. Taille max: {}, Types acceptés: {}", log.info("Composant de téléchargement initialisé. Taille max: {}, Types acceptés: {}",
maxFileSize, acceptedTypes); maxFileSize, acceptedTypes);
} }
/** /**
* Gère l'événement de téléchargement de fichier. * Gère l'événement de téléchargement de fichier.
* *
* @param event L'événement PrimeFaces contenant le fichier téléchargé. * @param event L'événement PrimeFaces contenant le fichier téléchargé.
*/ */
public void handleFileUpload(@NotNull FileUploadEvent event) { public void handleFileUpload(@NotNull FileUploadEvent event) {
UploadedFile file = event.getFile(); UploadedFile file = event.getFile();
log.info("Téléchargement de fichier : {}", file.getFileName()); log.info("Téléchargement de fichier : {}", file.getFileName());
try { try {
validateUploadRequest(file); validateUploadRequest(file);
UploadedFileInfo fileInfo = processUploadedFile(file); UploadedFileInfo fileInfo = processUploadedFile(file);
uploadedFiles.add(fileInfo); uploadedFiles.add(fileInfo);
addSuccessMessage("Fichier téléchargé avec succès : " + fileInfo.getFileName()); addSuccessMessage("Fichier téléchargé avec succès : " + fileInfo.getFileName());
} catch (FileUploadException e) { } catch (FileUploadException e) {
log.error("Erreur de validation du fichier : {}", file.getFileName(), e); log.error("Erreur de validation du fichier : {}", file.getFileName(), e);
addErrorMessage(e.getMessage()); addErrorMessage(e.getMessage());
} catch (IOException e) { } catch (IOException e) {
log.error("Erreur lors du traitement du fichier : {}", file.getFileName(), e); log.error("Erreur lors du traitement du fichier : {}", file.getFileName(), e);
addErrorMessage("Une erreur est survenue lors du traitement du fichier."); addErrorMessage("Une erreur est survenue lors du traitement du fichier.");
} }
} }
/** /**
* Valide la requête de téléchargement. * Valide la requête de téléchargement.
* *
* @param file Le fichier téléchargé. * @param file Le fichier téléchargé.
*/ */
private void validateUploadRequest(UploadedFile file) { private void validateUploadRequest(UploadedFile file) {
if (uploadedFiles.size() >= MAX_FILES) { if (uploadedFiles.size() >= MAX_FILES) {
throw new FileUploadException("Vous avez atteint le nombre maximum de fichiers autorisés."); throw new FileUploadException("Vous avez atteint le nombre maximum de fichiers autorisés.");
} }
fileValidator.validateFile(file, acceptedTypes, maxFileSize); fileValidator.validateFile(file, acceptedTypes, maxFileSize);
} }
/** /**
* Traite et stocke le fichier téléchargé. * Traite et stocke le fichier téléchargé.
* *
* @param file Le fichier téléchargé. * @param file Le fichier téléchargé.
* @return Les informations du fichier. * @return Les informations du fichier.
* @throws IOException En cas d'erreur de stockage. * @throws IOException En cas d'erreur de stockage.
*/ */
private UploadedFileInfo processUploadedFile(UploadedFile file) throws IOException { private UploadedFileInfo processUploadedFile(UploadedFile file) throws IOException {
String secureFileName = generateSecureFileName(file.getFileName()); String secureFileName = generateSecureFileName(file.getFileName());
Path destinationPath = storageService.storeFile(file.getInputStream(), uploadDirectory, secureFileName); Path destinationPath = storageService.storeFile(file.getInputStream(), uploadDirectory, secureFileName);
return UploadedFileInfo.builder() return UploadedFileInfo.builder()
.id(UUID.randomUUID().toString()) .id(UUID.randomUUID().toString())
.fileName(file.getFileName()) .fileName(file.getFileName())
.contentType(file.getContentType()) .contentType(file.getContentType())
.size(file.getSize()) .size(file.getSize())
.path(destinationPath) .path(destinationPath)
.build(); .build();
} }
/** /**
* Génère un nom de fichier sécurisé. * Génère un nom de fichier sécurisé.
* *
* @param originalFileName Nom original. * @param originalFileName Nom original.
* @return Nom sécurisé. * @return Nom sécurisé.
*/ */
private String generateSecureFileName(String originalFileName) { private String generateSecureFileName(String originalFileName) {
String extension = getFileExtension(originalFileName); String extension = getFileExtension(originalFileName);
return securityUtils.sanitizeFileName(UUID.randomUUID().toString() + "." + extension); return securityUtils.sanitizeFileName(UUID.randomUUID().toString() + "." + extension);
} }
/** /**
* Récupère l'extension d'un fichier. * Récupère l'extension d'un fichier.
* *
* @param fileName Nom du fichier. * @param fileName Nom du fichier.
* @return Extension. * @return Extension.
*/ */
private String getFileExtension(String fileName) { private String getFileExtension(String fileName) {
return fileName.substring(fileName.lastIndexOf('.') + 1); return fileName.substring(fileName.lastIndexOf('.') + 1);
} }
/** /**
* Crée un répertoire temporaire pour les téléchargements. * Crée un répertoire temporaire pour les téléchargements.
* *
* @return Le chemin du répertoire temporaire. * @return Le chemin du répertoire temporaire.
*/ */
private String createTempUploadDirectory() { private String createTempUploadDirectory() {
return storageService.createTempDirectory(TEMP_DIR_PREFIX + UUID.randomUUID()); return storageService.createTempDirectory(TEMP_DIR_PREFIX + UUID.randomUUID());
} }
/** /**
* Nettoie les ressources lors de la destruction du composant. * Nettoie les ressources lors de la destruction du composant.
*/ */
@PreDestroy @PreDestroy
public void cleanup() { public void cleanup() {
try { try {
storageService.deleteDirectory(uploadDirectory); storageService.deleteDirectory(uploadDirectory);
log.info("Répertoire temporaire supprimé : {}", uploadDirectory); log.info("Répertoire temporaire supprimé : {}", uploadDirectory);
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors du nettoyage des ressources : {}", uploadDirectory, e); log.error("Erreur lors du nettoyage des ressources : {}", uploadDirectory, e);
} }
} }
/** /**
* Ajoute un message de succès dans l'interface utilisateur. * Ajoute un message de succès dans l'interface utilisateur.
* *
* @param message Message à afficher. * @param message Message à afficher.
*/ */
private void addSuccessMessage(String message) { private void addSuccessMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message)); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, "Succès", message));
} }
/** /**
* Ajoute un message d'erreur dans l'interface utilisateur. * Ajoute un message d'erreur dans l'interface utilisateur.
* *
* @param message Message à afficher. * @param message Message à afficher.
*/ */
private void addErrorMessage(String message) { private void addErrorMessage(String message) {
FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message)); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", message));
} }
/** /**
* Classe interne représentant un fichier téléchargé. * Classe interne représentant un fichier téléchargé.
*/ */
@Getter @Getter
@Builder @Builder
public static class UploadedFileInfo { public static class UploadedFileInfo {
private final String id; private final String id;
private final String fileName; private final String fileName;
private final String contentType; private final String contentType;
private final long size; private final long size;
private final Path path; private final Path path;
} }
} }

View File

@@ -1,225 +1,225 @@
package dev.lions.components; package dev.lions.components;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.faces.model.SelectItem; import jakarta.faces.model.SelectItem;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named; import jakarta.inject.Named;
import jakarta.faces.context.FacesContext; import jakarta.faces.context.FacesContext;
import jakarta.faces.application.FacesMessage; import jakarta.faces.application.FacesMessage;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.io.Serial; import java.io.Serial;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import dev.lions.utils.FilterOperator; import dev.lions.utils.FilterOperator;
import dev.lions.utils.FilterCriteria; import dev.lions.utils.FilterCriteria;
import dev.lions.exceptions.FilterException; import dev.lions.exceptions.FilterException;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.*; import java.util.*;
/** /**
* Composant de gestion des filtres dynamiques. * Composant de gestion des filtres dynamiques.
* Permet la création, la validation et l'application de filtres * Permet la création, la validation et l'application de filtres
* pour des tableaux de données. * pour des tableaux de données.
* *
* <p>Fonctionnalités incluses : * <p>Fonctionnalités incluses :
* <ul> * <ul>
* <li>Ajout de filtres avec validation des entrées</li> * <li>Ajout de filtres avec validation des entrées</li>
* <li>Suppression de filtres</li> * <li>Suppression de filtres</li>
* <li>Application des filtres sur des listes d'objets</li> * <li>Application des filtres sur des listes d'objets</li>
* <li>Interface utilisateur avec feedback via les messages JSF</li> * <li>Interface utilisateur avec feedback via les messages JSF</li>
* </ul> * </ul>
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 2.2 * @version 2.2
*/ */
@Slf4j @Slf4j
@Named @Named
@ViewScoped @ViewScoped
public class FilterComponent implements Serializable { public class FilterComponent implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final int MAX_FILTERS = 10; private static final int MAX_FILTERS = 10;
@Getter @Setter @Getter @Setter
private List<FilterCriteria> criteria = new ArrayList<>(); private List<FilterCriteria> criteria = new ArrayList<>();
@Getter @Setter @Getter @Setter
private String selectedField; private String selectedField;
@Getter @Setter @Getter @Setter
private FilterOperator selectedOperator; private FilterOperator selectedOperator;
@Getter @Setter @Getter @Setter
private String filterValue; private String filterValue;
@Getter @Getter
private List<SelectItem> availableFields; private List<SelectItem> availableFields;
@Getter @Getter
private List<SelectItem> availableOperators; private List<SelectItem> availableOperators;
private final Map<String, String> fieldConfigurations = new LinkedHashMap<>(); private final Map<String, String> fieldConfigurations = new LinkedHashMap<>();
@PostConstruct @PostConstruct
public void init() { public void init() {
log.debug("Initialisation du composant de filtrage"); log.debug("Initialisation du composant de filtrage");
initializeFieldConfigurations(); initializeFieldConfigurations();
initializeAvailableFields(); initializeAvailableFields();
initializeAvailableOperators(); initializeAvailableOperators();
} }
/** /**
* Initialise la configuration des champs disponibles. * Initialise la configuration des champs disponibles.
*/ */
private void initializeFieldConfigurations() { private void initializeFieldConfigurations() {
fieldConfigurations.put("name", "Nom"); fieldConfigurations.put("name", "Nom");
fieldConfigurations.put("date", "Date"); fieldConfigurations.put("date", "Date");
fieldConfigurations.put("status", "Statut"); fieldConfigurations.put("status", "Statut");
fieldConfigurations.put("category", "Catégorie"); fieldConfigurations.put("category", "Catégorie");
fieldConfigurations.put("price", "Prix"); fieldConfigurations.put("price", "Prix");
log.info("Champs disponibles pour le filtrage : {}", fieldConfigurations.keySet()); log.info("Champs disponibles pour le filtrage : {}", fieldConfigurations.keySet());
} }
/** /**
* Remplit la liste des champs disponibles. * Remplit la liste des champs disponibles.
*/ */
private void initializeAvailableFields() { private void initializeAvailableFields() {
availableFields = new ArrayList<>(); availableFields = new ArrayList<>();
fieldConfigurations.forEach((key, value) -> fieldConfigurations.forEach((key, value) ->
availableFields.add(new SelectItem(key, value)) availableFields.add(new SelectItem(key, value))
); );
} }
/** /**
* Remplit la liste des opérateurs disponibles. * Remplit la liste des opérateurs disponibles.
*/ */
private void initializeAvailableOperators() { private void initializeAvailableOperators() {
availableOperators = new ArrayList<>(); availableOperators = new ArrayList<>();
for (FilterOperator operator : FilterOperator.values()) { for (FilterOperator operator : FilterOperator.values()) {
availableOperators.add(new SelectItem(operator, operator.getLabel())); availableOperators.add(new SelectItem(operator, operator.getLabel()));
} }
} }
/** /**
* Ajoute un critère de filtrage après validation. * Ajoute un critère de filtrage après validation.
*/ */
public void addFilter() { public void addFilter() {
log.debug("Ajout d'un filtre : Champ = {}, Opérateur = {}, Valeur = {}", log.debug("Ajout d'un filtre : Champ = {}, Opérateur = {}, Valeur = {}",
selectedField, selectedOperator, filterValue); selectedField, selectedOperator, filterValue);
try { try {
validateFilterInput(); validateFilterInput();
validateFilterLimit(); validateFilterLimit();
FilterCriteria newCriteria = new FilterCriteria(selectedField, selectedOperator, filterValue); FilterCriteria newCriteria = new FilterCriteria(selectedField, selectedOperator, filterValue);
criteria.add(newCriteria); criteria.add(newCriteria);
addMessage(FacesMessage.SEVERITY_INFO, "Filtre ajouté", "Filtre appliqué avec succès."); addMessage(FacesMessage.SEVERITY_INFO, "Filtre ajouté", "Filtre appliqué avec succès.");
log.info("Filtre ajouté avec succès : {}", newCriteria); log.info("Filtre ajouté avec succès : {}", newCriteria);
resetForm(); resetForm();
} catch (FilterException e) { } catch (FilterException e) {
log.warn("Erreur de validation du filtre", e); log.warn("Erreur de validation du filtre", e);
addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage()); addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", e.getMessage());
} }
} }
/** /**
* Supprime un critère de filtrage. * Supprime un critère de filtrage.
* *
* @param filter Le critère à supprimer. * @param filter Le critère à supprimer.
*/ */
public void removeFilter(@NotNull FilterCriteria filter) { public void removeFilter(@NotNull FilterCriteria filter) {
log.debug("Suppression du filtre : {}", filter); log.debug("Suppression du filtre : {}", filter);
criteria.remove(filter); criteria.remove(filter);
addMessage(FacesMessage.SEVERITY_INFO, "Filtre supprimé", "Le filtre a été retiré."); addMessage(FacesMessage.SEVERITY_INFO, "Filtre supprimé", "Le filtre a été retiré.");
} }
/** /**
* Efface tous les filtres existants. * Efface tous les filtres existants.
*/ */
public void clearAllFilters() { public void clearAllFilters() {
log.info("Suppression de tous les filtres ({})", criteria.size()); log.info("Suppression de tous les filtres ({})", criteria.size());
criteria.clear(); criteria.clear();
addMessage(FacesMessage.SEVERITY_INFO, "Filtres effacés", "Tous les filtres ont été supprimés."); addMessage(FacesMessage.SEVERITY_INFO, "Filtres effacés", "Tous les filtres ont été supprimés.");
} }
/** /**
* Applique les filtres sur une liste de données. * Applique les filtres sur une liste de données.
* *
* @param data Liste des objets à filtrer. * @param data Liste des objets à filtrer.
* @return Liste filtrée. * @return Liste filtrée.
*/ */
public List<Object> applyFilters(List<Object> data) { public List<Object> applyFilters(List<Object> data) {
if (criteria.isEmpty()) { if (criteria.isEmpty()) {
return data; return data;
} }
log.debug("Application des filtres sur {} éléments", data.size()); log.debug("Application des filtres sur {} éléments", data.size());
return data.stream().filter(this::matchesAllCriteria).toList(); return data.stream().filter(this::matchesAllCriteria).toList();
} }
/** /**
* Valide les entrées du filtre. * Valide les entrées du filtre.
*/ */
private void validateFilterInput() { private void validateFilterInput() {
if (selectedField == null || selectedOperator == null || filterValue == null) { if (selectedField == null || selectedOperator == null || filterValue == null) {
throw new FilterException("Tous les champs du filtre doivent être remplis."); throw new FilterException("Tous les champs du filtre doivent être remplis.");
} }
if (selectedOperator.isNumericComparison()) { if (selectedOperator.isNumericComparison()) {
try { try {
Double.parseDouble(filterValue); Double.parseDouble(filterValue);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new FilterException("La valeur doit être numérique pour cet opérateur."); throw new FilterException("La valeur doit être numérique pour cet opérateur.");
} }
} }
} }
private void validateFilterLimit() { private void validateFilterLimit() {
if (criteria.size() >= MAX_FILTERS) { if (criteria.size() >= MAX_FILTERS) {
throw new FilterException("Nombre maximum de filtres atteint (" + MAX_FILTERS + ")"); throw new FilterException("Nombre maximum de filtres atteint (" + MAX_FILTERS + ")");
} }
} }
private boolean matchesAllCriteria(Object item) { private boolean matchesAllCriteria(Object item) {
return criteria.stream().allMatch(filter -> matchesCriteria(item, filter)); return criteria.stream().allMatch(filter -> matchesCriteria(item, filter));
} }
private boolean matchesCriteria(Object item, FilterCriteria filter) { private boolean matchesCriteria(Object item, FilterCriteria filter) {
try { try {
Object value = getPropertyValue(item, filter.getField()); Object value = getPropertyValue(item, filter.getField());
return filter.getOperator().apply(value, (String) filter.getValue()); return filter.getOperator().apply(value, (String) filter.getValue());
} catch (Exception e) { } catch (Exception e) {
log.warn("Erreur d'accès à la propriété : {}", filter.getField(), e); log.warn("Erreur d'accès à la propriété : {}", filter.getField(), e);
return false; return false;
} }
} }
private Object getPropertyValue(Object item, String property) { private Object getPropertyValue(Object item, String property) {
try { try {
Method getter = item.getClass().getMethod("get" + capitalize(property)); Method getter = item.getClass().getMethod("get" + capitalize(property));
return getter.invoke(item); return getter.invoke(item);
} catch (Exception e) { } catch (Exception e) {
throw new FilterException("Propriété inaccessible : " + property); throw new FilterException("Propriété inaccessible : " + property);
} }
} }
private String capitalize(String str) { private String capitalize(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1); return str.substring(0, 1).toUpperCase() + str.substring(1);
} }
private void resetForm() { private void resetForm() {
selectedField = null; selectedField = null;
selectedOperator = null; selectedOperator = null;
filterValue = null; filterValue = null;
} }
private void addMessage(FacesMessage.Severity severity, String summary, String detail) { private void addMessage(FacesMessage.Severity severity, String summary, String detail) {
FacesContext.getCurrentInstance() FacesContext.getCurrentInstance()
.addMessage(null, new FacesMessage(severity, summary, detail)); .addMessage(null, new FacesMessage(severity, summary, detail));
} }
} }

View File

@@ -1,84 +1,84 @@
package dev.lions.components; package dev.lions.components;
import jakarta.faces.application.FacesMessage; import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext; import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped; import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Named; import jakarta.inject.Named;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import java.io.Serial; import java.io.Serial;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.io.Serializable; import java.io.Serializable;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/** /**
* Composant gérant l'affichage des notifications dans l'interface utilisateur. * Composant gérant l'affichage des notifications dans l'interface utilisateur.
*/ */
@Slf4j @Slf4j
@Named @Named
@ViewScoped @ViewScoped
public class NotificationComponent implements Serializable { public class NotificationComponent implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final String MESSAGE_BUNDLE = "messages"; private static final String MESSAGE_BUNDLE = "messages";
@Inject @Inject
FacesContext facesContext; FacesContext facesContext;
@Inject @Inject
transient ResourceBundle messageBundle; transient ResourceBundle messageBundle;
/** /**
* Affiche un message de succès. * Affiche un message de succès.
*/ */
public void showSuccess(@NotBlank String key) { public void showSuccess(@NotBlank String key) {
log.debug("Affichage message succès: {}", key); log.debug("Affichage message succès: {}", key);
addMessage(FacesMessage.SEVERITY_INFO, addMessage(FacesMessage.SEVERITY_INFO,
getMessage(key + ".title", "Succès"), getMessage(key + ".title", "Succès"),
getMessage(key + ".detail")); getMessage(key + ".detail"));
} }
/** /**
* Affiche un message d'erreur. * Affiche un message d'erreur.
*/ */
public void showError(@NotBlank String key) { public void showError(@NotBlank String key) {
log.debug("Affichage message erreur: {}", key); log.debug("Affichage message erreur: {}", key);
addMessage(FacesMessage.SEVERITY_ERROR, addMessage(FacesMessage.SEVERITY_ERROR,
getMessage(key + ".title", "Erreur"), getMessage(key + ".title", "Erreur"),
getMessage(key + ".detail")); getMessage(key + ".detail"));
} }
/** /**
* Affiche un message d'avertissement. * Affiche un message d'avertissement.
*/ */
public void showWarning(@NotBlank String key) { public void showWarning(@NotBlank String key) {
log.debug("Affichage message avertissement: {}", key); log.debug("Affichage message avertissement: {}", key);
addMessage(FacesMessage.SEVERITY_WARN, addMessage(FacesMessage.SEVERITY_WARN,
getMessage(key + ".title", "Attention"), getMessage(key + ".title", "Attention"),
getMessage(key + ".detail")); getMessage(key + ".detail"));
} }
/** /**
* Récupère un message localisé avec fallback. * Récupère un message localisé avec fallback.
*/ */
private String getMessage(String key, String defaultValue) { private String getMessage(String key, String defaultValue) {
try { try {
return messageBundle.getString(key); return messageBundle.getString(key);
} catch (Exception e) { } catch (Exception e) {
log.warn("Message non trouvé: {}", key); log.warn("Message non trouvé: {}", key);
return defaultValue; return defaultValue;
} }
} }
private String getMessage(String key) { private String getMessage(String key) {
return getMessage(key, key); return getMessage(key, key);
} }
private void addMessage(FacesMessage.Severity severity, String summary, String detail) { private void addMessage(FacesMessage.Severity severity, String summary, String detail) {
facesContext.addMessage(null, new FacesMessage(severity, summary, detail)); facesContext.addMessage(null, new FacesMessage(severity, summary, detail));
log.debug("Message ajouté: {} - {}", summary, detail); log.debug("Message ajouté: {} - {}", summary, detail);
} }
} }

View File

@@ -1,339 +1,339 @@
package dev.lions.config; package dev.lions.config;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import dev.lions.exceptions.ConfigurationException; import dev.lions.exceptions.ConfigurationException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* Configuration centrale de l'application Lions Dev. * Configuration centrale de l'application Lions Dev.
* Cette classe gère l'ensemble des paramètres de configuration de manière thread-safe * Cette classe gère l'ensemble des paramètres de configuration de manière thread-safe
* et fournit une interface unifiée pour accéder aux différentes configurations. * et fournit une interface unifiée pour accéder aux différentes configurations.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 2.0 * @version 2.0
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
@Getter @Getter
public class ApplicationConfig { public class ApplicationConfig {
/** /**
* Énumération des environnements d'exécution supportés. * Énumération des environnements d'exécution supportés.
*/ */
public enum Environment { public enum Environment {
DEVELOPMENT("development"), DEVELOPMENT("development"),
STAGING("staging"), STAGING("staging"),
PRODUCTION("production"); PRODUCTION("production");
private final String value; private final String value;
Environment(String value) { Environment(String value) {
this.value = value; this.value = value;
} }
public String getValue() { public String getValue() {
return value; return value;
} }
public static Environment fromString(String value) { public static Environment fromString(String value) {
return Arrays.stream(values()) return Arrays.stream(values())
.filter(env -> env.getValue().equalsIgnoreCase(value)) .filter(env -> env.getValue().equalsIgnoreCase(value))
.findFirst() .findFirst()
.orElse(DEVELOPMENT); .orElse(DEVELOPMENT);
} }
} }
// Constantes de configuration // Constantes de configuration
private static final String DEFAULT_ENVIRONMENT = "development"; private static final String DEFAULT_ENVIRONMENT = "development";
private static final long DEFAULT_MAX_FILE_SIZE = 10_485_760L; // 10MB private static final long DEFAULT_MAX_FILE_SIZE = 10_485_760L; // 10MB
private static final int DEFAULT_CACHE_SIZE = 1000; private static final int DEFAULT_CACHE_SIZE = 1000;
private static final int MIN_PORT = 1; private static final int MIN_PORT = 1;
private static final int MAX_PORT = 65535; private static final int MAX_PORT = 65535;
// Configuration de base de l'application // Configuration de base de l'application
@Inject @Inject
@ConfigProperty(name = "app.name", defaultValue = "Lions Dev") @ConfigProperty(name = "app.name", defaultValue = "Lions Dev")
private String applicationName; private String applicationName;
@Inject @Inject
@ConfigProperty(name = "app.environment", defaultValue = DEFAULT_ENVIRONMENT) @ConfigProperty(name = "app.environment", defaultValue = DEFAULT_ENVIRONMENT)
private String environment; private String environment;
@NotBlank @NotBlank
@Inject @Inject
@ConfigProperty(name = "app.base-url") @ConfigProperty(name = "app.base-url")
private String baseUrl; private String baseUrl;
// Configuration du stockage // Configuration du stockage
@NotBlank @NotBlank
@Inject @Inject
@ConfigProperty(name = "app.storage.base-path") @ConfigProperty(name = "app.storage.base-path")
private String storageBasePath; private String storageBasePath;
@NotBlank @NotBlank
@Inject @Inject
@ConfigProperty(name = "app.storage.images.path", defaultValue = "images") @ConfigProperty(name = "app.storage.images.path", defaultValue = "images")
private String imageStoragePath; private String imageStoragePath;
@Inject @Inject
@ConfigProperty(name = "app.storage.allowed-types", defaultValue = "jpg,jpeg,png,gif") @ConfigProperty(name = "app.storage.allowed-types", defaultValue = "jpg,jpeg,png,gif")
private String allowedFileTypes; private String allowedFileTypes;
@Min(1_048_576L) // 1MB minimum @Min(1_048_576L) // 1MB minimum
@Max(104_857_600L) // 100MB maximum @Max(104_857_600L) // 100MB maximum
@Inject @Inject
@ConfigProperty(name = "app.storage.max-size", defaultValue = "10485760") @ConfigProperty(name = "app.storage.max-size", defaultValue = "10485760")
private Long maxFileSize; private Long maxFileSize;
// Configuration des emails // Configuration des emails
@NotBlank @NotBlank
@Inject @Inject
@ConfigProperty(name = "app.email.from") @ConfigProperty(name = "app.email.from")
private String emailFrom; private String emailFrom;
@NotBlank @NotBlank
@Inject @Inject
@ConfigProperty(name = "app.email.support") @ConfigProperty(name = "app.email.support")
private String emailSupport; private String emailSupport;
@Inject @Inject
@ConfigProperty(name = "app.email.template-path", defaultValue = "templates/email") @ConfigProperty(name = "app.email.template-path", defaultValue = "templates/email")
private String emailTemplatePath; private String emailTemplatePath;
// Configuration SMTP // Configuration SMTP
@NotBlank @NotBlank
@Inject @Inject
@ConfigProperty(name = "app.smtp.host") @ConfigProperty(name = "app.smtp.host")
private String smtpHost; private String smtpHost;
@Min(MIN_PORT) @Min(MIN_PORT)
@Max(MAX_PORT) @Max(MAX_PORT)
@Inject @Inject
@ConfigProperty(name = "app.smtp.port") @ConfigProperty(name = "app.smtp.port")
private Integer smtpPort; private Integer smtpPort;
@Inject @Inject
@ConfigProperty(name = "app.smtp.username") @ConfigProperty(name = "app.smtp.username")
private Optional<String> smtpUsername; private Optional<String> smtpUsername;
@Inject @Inject
@ConfigProperty(name = "app.smtp.password") @ConfigProperty(name = "app.smtp.password")
private Optional<String> smtpPassword; private Optional<String> smtpPassword;
@NotBlank @NotBlank
@Inject @Inject
@ConfigProperty(name = "app.admin.email") @ConfigProperty(name = "app.admin.email")
private String adminEmailAddress; private String adminEmailAddress;
// Collections thread-safe pour les configurations dynamiques // Collections thread-safe pour les configurations dynamiques
private final Map<String, String> applicationUrls = new ConcurrentHashMap<>(); private final Map<String, String> applicationUrls = new ConcurrentHashMap<>();
private final Map<Environment, String> environmentConfigs = new EnumMap<>(Environment.class); private final Map<Environment, String> environmentConfigs = new EnumMap<>(Environment.class);
private List<String> allowedFileTypesList; private List<String> allowedFileTypesList;
/** /**
* Initialise la configuration après l'injection des propriétés. * Initialise la configuration après l'injection des propriétés.
* Valide et prépare l'ensemble des paramètres de configuration. * Valide et prépare l'ensemble des paramètres de configuration.
* *
* @throws ConfigurationException si la configuration est invalide * @throws ConfigurationException si la configuration est invalide
*/ */
@PostConstruct @PostConstruct
void initialize() { void initialize() {
try { try {
log.info("Initialisation de la configuration de l'application: {}", applicationName); log.info("Initialisation de la configuration de l'application: {}", applicationName);
validateConfiguration(); validateConfiguration();
initializeApplicationUrls(); initializeApplicationUrls();
initializeAllowedFileTypes(); initializeAllowedFileTypes();
initializeEnvironmentConfigs(); initializeEnvironmentConfigs();
log.info("Configuration initialisée avec succès en environnement: {}", environment); log.info("Configuration initialisée avec succès en environnement: {}", environment);
} catch (Exception e) { } catch (Exception e) {
String errorMessage = "Erreur lors de l'initialisation de la configuration"; String errorMessage = "Erreur lors de l'initialisation de la configuration";
log.error(errorMessage, e); log.error(errorMessage, e);
throw new ConfigurationException(errorMessage, e); throw new ConfigurationException(errorMessage, e);
} }
} }
/** /**
* Valide l'ensemble de la configuration. * Valide l'ensemble de la configuration.
* *
* @throws ConfigurationException si la validation échoue * @throws ConfigurationException si la validation échoue
*/ */
private void validateConfiguration() { private void validateConfiguration() {
log.debug("Validation de la configuration"); log.debug("Validation de la configuration");
validateEnvironment(); validateEnvironment();
validateStoragePaths(); validateStoragePaths();
validateSmtpConfiguration(); validateSmtpConfiguration();
validateFileSize(); validateFileSize();
} }
/** /**
* Valide l'environnement d'exécution. * Valide l'environnement d'exécution.
*/ */
private void validateEnvironment() { private void validateEnvironment() {
if (!isValidEnvironment(environment)) { if (!isValidEnvironment(environment)) {
throw new ConfigurationException("Environnement non reconnu: " + environment); throw new ConfigurationException("Environnement non reconnu: " + environment);
} }
} }
/** /**
* Valide les chemins de stockage. * Valide les chemins de stockage.
*/ */
private void validateStoragePaths() { private void validateStoragePaths() {
Path basePath = Paths.get(storageBasePath); Path basePath = Paths.get(storageBasePath);
validatePath(basePath, "stockage principal"); validatePath(basePath, "stockage principal");
Path imagesPath = basePath.resolve(imageStoragePath); Path imagesPath = basePath.resolve(imageStoragePath);
validatePath(imagesPath, "stockage des images"); validatePath(imagesPath, "stockage des images");
} }
/** /**
* Valide un chemin spécifique. * Valide un chemin spécifique.
*/ */
private void validatePath(Path path, String description) { private void validatePath(Path path, String description) {
if (!path.toFile().exists() && !path.toFile().mkdirs()) { if (!path.toFile().exists() && !path.toFile().mkdirs()) {
throw new ConfigurationException( throw new ConfigurationException(
"Impossible de créer le répertoire de " + description + ": " + path); "Impossible de créer le répertoire de " + description + ": " + path);
} }
} }
/** /**
* Valide la configuration SMTP. * Valide la configuration SMTP.
*/ */
private void validateSmtpConfiguration() { private void validateSmtpConfiguration() {
if (isSmtpConfigured() && (smtpPort < MIN_PORT || smtpPort > MAX_PORT)) { if (isSmtpConfigured() && (smtpPort < MIN_PORT || smtpPort > MAX_PORT)) {
throw new ConfigurationException("Port SMTP invalide: " + smtpPort); throw new ConfigurationException("Port SMTP invalide: " + smtpPort);
} }
} }
/** /**
* Valide la taille maximale des fichiers. * Valide la taille maximale des fichiers.
*/ */
private void validateFileSize() { private void validateFileSize() {
if (maxFileSize <= 0) { if (maxFileSize <= 0) {
throw new ConfigurationException( throw new ConfigurationException(
"Taille maximale de fichier invalide: " + maxFileSize); "Taille maximale de fichier invalide: " + maxFileSize);
} }
} }
/** /**
* Récupère l'adresse email système (expéditeur par défaut). * Récupère l'adresse email système (expéditeur par défaut).
* *
* @return Adresse email système * @return Adresse email système
*/ */
public String getSystemEmailAddress() { public String getSystemEmailAddress() {
return emailFrom; return emailFrom;
} }
/** /**
* Vérifie si le SSL est activé pour le serveur SMTP. * Vérifie si le SSL est activé pour le serveur SMTP.
* *
* @return true si SSL est activé, sinon false * @return true si SSL est activé, sinon false
*/ */
public boolean isSmtpSslEnabled() { public boolean isSmtpSslEnabled() {
return smtpPort == 465; // Port 465 est commun pour SMTP avec SSL return smtpPort == 465; // Port 465 est commun pour SMTP avec SSL
} }
/** /**
* Initialise les URLs de l'application. * Initialise les URLs de l'application.
*/ */
private void initializeApplicationUrls() { private void initializeApplicationUrls() {
applicationUrls.clear(); applicationUrls.clear();
applicationUrls.put("home", "/"); applicationUrls.put("home", "/");
applicationUrls.put("services", "/services"); applicationUrls.put("services", "/services");
applicationUrls.put("contact", "/contact"); applicationUrls.put("contact", "/contact");
applicationUrls.put("admin", "/admin"); applicationUrls.put("admin", "/admin");
applicationUrls.put("projects", "/projects"); applicationUrls.put("projects", "/projects");
applicationUrls.put("portfolio", "/portfolio"); applicationUrls.put("portfolio", "/portfolio");
} }
/** /**
* Initialise la liste des types de fichiers autorisés. * Initialise la liste des types de fichiers autorisés.
*/ */
private void initializeAllowedFileTypes() { private void initializeAllowedFileTypes() {
allowedFileTypesList = Collections.unmodifiableList( allowedFileTypesList = Collections.unmodifiableList(
Arrays.asList(allowedFileTypes.toLowerCase().split(",")) Arrays.asList(allowedFileTypes.toLowerCase().split(","))
); );
} }
/** /**
* Initialise les configurations spécifiques aux environnements. * Initialise les configurations spécifiques aux environnements.
*/ */
private void initializeEnvironmentConfigs() { private void initializeEnvironmentConfigs() {
environmentConfigs.put(Environment.DEVELOPMENT, "dev"); environmentConfigs.put(Environment.DEVELOPMENT, "dev");
environmentConfigs.put(Environment.STAGING, "stage"); environmentConfigs.put(Environment.STAGING, "stage");
environmentConfigs.put(Environment.PRODUCTION, "prod"); environmentConfigs.put(Environment.PRODUCTION, "prod");
} }
// Méthodes publiques utilitaires // Méthodes publiques utilitaires
/** /**
* Récupère le chemin complet pour le stockage des images. * Récupère le chemin complet pour le stockage des images.
*/ */
public String getImageStoragePath() { public String getImageStoragePath() {
return Paths.get(storageBasePath, imageStoragePath).toString(); return Paths.get(storageBasePath, imageStoragePath).toString();
} }
/** /**
* Vérifie si un type de fichier est autorisé. * Vérifie si un type de fichier est autorisé.
*/ */
public boolean isFileTypeAllowed(String fileType) { public boolean isFileTypeAllowed(String fileType) {
return fileType != null && allowedFileTypesList.contains(fileType.toLowerCase().trim()); return fileType != null && allowedFileTypesList.contains(fileType.toLowerCase().trim());
} }
/** /**
* Récupère l'URL d'une section de l'application. * Récupère l'URL d'une section de l'application.
*/ */
public String getUrl(String key) { public String getUrl(String key) {
return applicationUrls.getOrDefault(key, "/"); return applicationUrls.getOrDefault(key, "/");
} }
/** /**
* Vérifie si l'environnement est en développement. * Vérifie si l'environnement est en développement.
*/ */
public boolean isDevelopment() { public boolean isDevelopment() {
return Environment.DEVELOPMENT.getValue().equals(environment); return Environment.DEVELOPMENT.getValue().equals(environment);
} }
/** /**
* Vérifie si l'environnement est en production. * Vérifie si l'environnement est en production.
*/ */
public boolean isProduction() { public boolean isProduction() {
return Environment.PRODUCTION.getValue().equals(environment); return Environment.PRODUCTION.getValue().equals(environment);
} }
/** /**
* Vérifie si la configuration SMTP est complète. * Vérifie si la configuration SMTP est complète.
*/ */
public boolean isSmtpConfigured() { public boolean isSmtpConfigured() {
return smtpUsername.isPresent() && smtpPassword.isPresent() && return smtpUsername.isPresent() && smtpPassword.isPresent() &&
smtpHost != null && !smtpHost.equals("localhost"); smtpHost != null && !smtpHost.equals("localhost");
} }
/** /**
* Vérifie si un environnement est valide. * Vérifie si un environnement est valide.
*/ */
private boolean isValidEnvironment(String env) { private boolean isValidEnvironment(String env) {
return Arrays.stream(Environment.values()) return Arrays.stream(Environment.values())
.anyMatch(e -> e.getValue().equals(env)); .anyMatch(e -> e.getValue().equals(env));
} }
} }

View File

@@ -1,180 +1,180 @@
package dev.lions.config; package dev.lions.config;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event; import jakarta.enterprise.event.Event;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import dev.lions.events.ConfigurationEvent; import dev.lions.events.ConfigurationEvent;
import dev.lions.exceptions.ConfigurationException; import dev.lions.exceptions.ConfigurationException;
import dev.lions.utils.EncryptionUtils; import dev.lions.utils.EncryptionUtils;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
/** /**
* Service de gestion avancée de la configuration de l'application. * Service de gestion avancée de la configuration de l'application.
* Fournit une interface enrichie pour accéder et gérer les paramètres de configuration * Fournit une interface enrichie pour accéder et gérer les paramètres de configuration
* de manière thread-safe, sécurisée et optimisée. * de manière thread-safe, sécurisée et optimisée.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 2.1 * @version 2.1
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class ApplicationConfigService { public class ApplicationConfigService {
private static final long CACHE_DURATION_MS = 300_000; // 5 minutes private static final long CACHE_DURATION_MS = 300_000; // 5 minutes
private static final long MIN_DISK_SPACE_BYTES = 100 * 1024 * 1024; // 100 MB private static final long MIN_DISK_SPACE_BYTES = 100 * 1024 * 1024; // 100 MB
private static final String[] MANDATORY_DIRECTORIES = {"logs", "data", "temp"}; private static final String[] MANDATORY_DIRECTORIES = {"logs", "data", "temp"};
private final ApplicationConfig applicationConfig; private final ApplicationConfig applicationConfig;
private final Map<String, CachedValue<Object>> configCache; private final Map<String, CachedValue<Object>> configCache;
private final AtomicLong lastHealthCheck; private final AtomicLong lastHealthCheck;
@Inject @Inject
private Event<ConfigurationEvent> configurationEvent; private Event<ConfigurationEvent> configurationEvent;
@Inject @Inject
private EncryptionUtils encryptionUtils; private EncryptionUtils encryptionUtils;
@Inject @Inject
public ApplicationConfigService(@NotNull ApplicationConfig applicationConfig) { public ApplicationConfigService(@NotNull ApplicationConfig applicationConfig) {
this.applicationConfig = applicationConfig; this.applicationConfig = applicationConfig;
this.configCache = new ConcurrentHashMap<>(); this.configCache = new ConcurrentHashMap<>();
this.lastHealthCheck = new AtomicLong(0); this.lastHealthCheck = new AtomicLong(0);
initializeService(); initializeService();
} }
/** /**
* Valide la configuration actuelle de l'application. * Valide la configuration actuelle de l'application.
* Vérifie les chemins de stockage, les répertoires obligatoires et les paramètres critiques. * Vérifie les chemins de stockage, les répertoires obligatoires et les paramètres critiques.
*/ */
private void validateConfiguration() { private void validateConfiguration() {
log.info("Validation de la configuration de l'application..."); log.info("Validation de la configuration de l'application...");
try { try {
validateStorageBasePath(); validateStorageBasePath();
validateMandatoryDirectories(); validateMandatoryDirectories();
validateSecuritySettings(); validateSecuritySettings();
log.info("Configuration de l'application validée avec succès."); log.info("Configuration de l'application validée avec succès.");
} catch (ConfigurationException e) { } catch (ConfigurationException e) {
log.error("Validation échouée : {}", e.getMessage()); log.error("Validation échouée : {}", e.getMessage());
throw e; // Relancer l'exception après log throw e; // Relancer l'exception après log
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur inattendue lors de la validation de la configuration", e); log.error("Erreur inattendue lors de la validation de la configuration", e);
throw new ConfigurationException("Erreur inattendue lors de la validation", e); throw new ConfigurationException("Erreur inattendue lors de la validation", e);
} }
} }
/** /**
* Vérifie l'existence et l'accessibilité du chemin de stockage. * Vérifie l'existence et l'accessibilité du chemin de stockage.
*/ */
private void validateStorageBasePath() { private void validateStorageBasePath() {
Path storagePath = Paths.get(applicationConfig.getStorageBasePath()); Path storagePath = Paths.get(applicationConfig.getStorageBasePath());
if (!Files.exists(storagePath) || !Files.isDirectory(storagePath)) { if (!Files.exists(storagePath) || !Files.isDirectory(storagePath)) {
throw new ConfigurationException("Le chemin de stockage est invalide : " + storagePath); throw new ConfigurationException("Le chemin de stockage est invalide : " + storagePath);
} }
log.debug("Chemin de stockage validé : {}", storagePath); log.debug("Chemin de stockage validé : {}", storagePath);
} }
/** /**
* Vérifie l'existence des répertoires obligatoires. * Vérifie l'existence des répertoires obligatoires.
*/ */
private void validateMandatoryDirectories() { private void validateMandatoryDirectories() {
String basePath = applicationConfig.getStorageBasePath(); String basePath = applicationConfig.getStorageBasePath();
for (String directory : MANDATORY_DIRECTORIES) { for (String directory : MANDATORY_DIRECTORIES) {
Path directoryPath = Paths.get(basePath, directory); Path directoryPath = Paths.get(basePath, directory);
if (!Files.exists(directoryPath) || !Files.isWritable(directoryPath)) { if (!Files.exists(directoryPath) || !Files.isWritable(directoryPath)) {
throw new ConfigurationException("Répertoire obligatoire manquant ou inaccessible : " + directoryPath); throw new ConfigurationException("Répertoire obligatoire manquant ou inaccessible : " + directoryPath);
} }
log.debug("Répertoire valide : {}", directoryPath); log.debug("Répertoire valide : {}", directoryPath);
} }
} }
/** /**
* Valide les paramètres de sécurité essentiels. * Valide les paramètres de sécurité essentiels.
*/ */
private void validateSecuritySettings() { private void validateSecuritySettings() {
if (applicationConfig.isProduction()) { if (applicationConfig.isProduction()) {
if (!applicationConfig.isSmtpConfigured()) { if (!applicationConfig.isSmtpConfigured()) {
throw new ConfigurationException("La configuration SMTP est obligatoire en production"); throw new ConfigurationException("La configuration SMTP est obligatoire en production");
} }
log.debug("Paramètres SMTP validés pour l'environnement production"); log.debug("Paramètres SMTP validés pour l'environnement production");
} }
log.debug("Paramètres de sécurité validés"); log.debug("Paramètres de sécurité validés");
} }
private void initializeService() { private void initializeService() {
try { try {
log.info("Initialisation du service de configuration"); log.info("Initialisation du service de configuration");
createMandatoryDirectories(); createMandatoryDirectories();
validateConfiguration(); validateConfiguration();
initializeCache(); initializeCache();
notifyServiceInitialized(); notifyServiceInitialized();
log.info("Service de configuration initialisé avec succès"); log.info("Service de configuration initialisé avec succès");
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de l'initialisation du service de configuration", e); log.error("Erreur lors de l'initialisation du service de configuration", e);
throw new ConfigurationException("Échec de l'initialisation du service", e); throw new ConfigurationException("Échec de l'initialisation du service", e);
} }
} }
private void createMandatoryDirectories() { private void createMandatoryDirectories() {
String basePath = applicationConfig.getStorageBasePath(); String basePath = applicationConfig.getStorageBasePath();
for (String directory : MANDATORY_DIRECTORIES) { for (String directory : MANDATORY_DIRECTORIES) {
Path directoryPath = Paths.get(basePath, directory); Path directoryPath = Paths.get(basePath, directory);
if (!Files.exists(directoryPath)) { if (!Files.exists(directoryPath)) {
try { try {
Files.createDirectories(directoryPath); Files.createDirectories(directoryPath);
log.debug("Répertoire créé : {}", directoryPath); log.debug("Répertoire créé : {}", directoryPath);
} catch (Exception e) { } catch (Exception e) {
throw new ConfigurationException("Impossible de créer le répertoire : " + directoryPath, e); throw new ConfigurationException("Impossible de créer le répertoire : " + directoryPath, e);
} }
} }
} }
} }
private void initializeCache() { private void initializeCache() {
log.info("Initialisation du cache de configuration..."); log.info("Initialisation du cache de configuration...");
configCache.clear(); configCache.clear();
} }
private void notifyServiceInitialized() { private void notifyServiceInitialized() {
ConfigurationEvent event = new ConfigurationEvent( ConfigurationEvent event = new ConfigurationEvent(
"SERVICE_INITIALIZED", "SERVICE_INITIALIZED",
Map.of("timestamp", System.currentTimeMillis(), Map.of("timestamp", System.currentTimeMillis(),
"environment", applicationConfig.getEnvironment()) "environment", applicationConfig.getEnvironment())
); );
configurationEvent.fire(event); configurationEvent.fire(event);
} }
/** /**
* Classe interne représentant une valeur mise en cache. * Classe interne représentant une valeur mise en cache.
*/ */
private static class CachedValue<T> { private static class CachedValue<T> {
private final T value; private final T value;
private final long timestamp; private final long timestamp;
CachedValue(T value) { CachedValue(T value) {
this.value = value; this.value = value;
this.timestamp = System.currentTimeMillis(); this.timestamp = System.currentTimeMillis();
} }
T getValue() { T getValue() {
return value; return value;
} }
} }
} }

View File

@@ -1,183 +1,183 @@
package dev.lions.config; package dev.lions.config;
import dev.lions.exceptions.ConfigurationException; import dev.lions.exceptions.ConfigurationException;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes; import jakarta.enterprise.event.Observes;
import jakarta.faces.annotation.FacesConfig; import jakarta.faces.annotation.FacesConfig;
import jakarta.faces.application.ViewHandler; import jakarta.faces.application.ViewHandler;
import jakarta.faces.component.UIViewRoot; import jakarta.faces.component.UIViewRoot;
import jakarta.faces.context.FacesContext; import jakarta.faces.context.FacesContext;
import jakarta.faces.event.PostConstructApplicationEvent; import jakarta.faces.event.PostConstructApplicationEvent;
import jakarta.faces.event.PreDestroyApplicationEvent; import jakarta.faces.event.PreDestroyApplicationEvent;
import jakarta.faces.event.SystemEvent; import jakarta.faces.event.SystemEvent;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.util.Map; import java.util.Map;
/** /**
* Configuration Jakarta Server Faces (JSF) de l'application. * Configuration Jakarta Server Faces (JSF) de l'application.
* Cette classe gère l'ensemble des paramètres et comportements spécifiques à JSF, * Cette classe gère l'ensemble des paramètres et comportements spécifiques à JSF,
* assurant une expérience utilisateur cohérente et performante. * assurant une expérience utilisateur cohérente et performante.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 2.1 * @version 2.1
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
@FacesConfig @FacesConfig
public class JSFConfiguration { public class JSFConfiguration {
@Inject @Inject
@ConfigProperty(name = "jakarta.faces.PROJECT_STAGE", defaultValue = "Development") @ConfigProperty(name = "jakarta.faces.PROJECT_STAGE", defaultValue = "Development")
String projectStage; String projectStage;
@Inject @Inject
@ConfigProperty(name = "jakarta.faces.FACELETS_REFRESH_PERIOD", defaultValue = "2") @ConfigProperty(name = "jakarta.faces.FACELETS_REFRESH_PERIOD", defaultValue = "2")
Integer faceletsRefreshPeriod; Integer faceletsRefreshPeriod;
@Inject @Inject
@ConfigProperty(name = "jakarta.faces.STATE_SAVING_METHOD", defaultValue = "server") @ConfigProperty(name = "jakarta.faces.STATE_SAVING_METHOD", defaultValue = "server")
String stateSavingMethod; String stateSavingMethod;
@Inject @Inject
@ConfigProperty(name = "primefaces.THEME", defaultValue = "saga") @ConfigProperty(name = "primefaces.THEME", defaultValue = "saga")
private String primefacesTheme; private String primefacesTheme;
@Inject @Inject
@ConfigProperty(name = "jakarta.faces.VALIDATE_EMPTY_FIELDS", defaultValue = "true") @ConfigProperty(name = "jakarta.faces.VALIDATE_EMPTY_FIELDS", defaultValue = "true")
private Boolean validateEmptyFields; private Boolean validateEmptyFields;
@Inject @Inject
@ConfigProperty(name = "jakarta.faces.FACELETS_SKIP_COMMENTS", defaultValue = "true") @ConfigProperty(name = "jakarta.faces.FACELETS_SKIP_COMMENTS", defaultValue = "true")
private Boolean skipComments; private Boolean skipComments;
/** /**
* Initialise la configuration JSF au démarrage de l'application. * Initialise la configuration JSF au démarrage de l'application.
* *
* @param event Événement de construction de l'application. * @param event Événement de construction de l'application.
*/ */
public void initialize(@Observes @NotNull PostConstructApplicationEvent event) { public void initialize(@Observes @NotNull PostConstructApplicationEvent event) {
try { try {
log.info("Initialisation de la configuration JSF"); log.info("Initialisation de la configuration JSF");
configureFacesContext(); configureFacesContext();
configureViewHandler(); configureViewHandler();
configurePrimeFaces(); configurePrimeFaces();
applyPerformanceOptimizations(); applyPerformanceOptimizations();
log.info("Configuration JSF initialisée avec succès - Mode: {}", projectStage); log.info("Configuration JSF initialisée avec succès - Mode: {}", projectStage);
} catch (Exception e) { } catch (Exception e) {
String message = "Erreur lors de l'initialisation de la configuration JSF"; String message = "Erreur lors de l'initialisation de la configuration JSF";
log.error(message, e); log.error(message, e);
throw new ConfigurationException(message, e); throw new ConfigurationException(message, e);
} }
} }
/** /**
* Nettoie les ressources JSF avant l'arrêt de l'application. * Nettoie les ressources JSF avant l'arrêt de l'application.
* *
* @param event Événement de destruction de l'application. * @param event Événement de destruction de l'application.
*/ */
public void cleanup(@Observes @NotNull PreDestroyApplicationEvent event) { public void cleanup(@Observes @NotNull PreDestroyApplicationEvent event) {
log.info("Nettoyage des ressources JSF"); log.info("Nettoyage des ressources JSF");
} }
/** /**
* Configure le contexte Faces avec les paramètres spécifiques. * Configure le contexte Faces avec les paramètres spécifiques.
*/ */
private void configureFacesContext() { private void configureFacesContext() {
log.debug("Configuration du contexte Faces"); log.debug("Configuration du contexte Faces");
setFacesParameter("jakarta.faces.PROJECT_STAGE", projectStage); setFacesParameter("jakarta.faces.PROJECT_STAGE", projectStage);
setFacesParameter("jakarta.faces.STATE_SAVING_METHOD", stateSavingMethod); setFacesParameter("jakarta.faces.STATE_SAVING_METHOD", stateSavingMethod);
setFacesParameter("jakarta.faces.FACELETS_REFRESH_PERIOD", faceletsRefreshPeriod.toString()); setFacesParameter("jakarta.faces.FACELETS_REFRESH_PERIOD", faceletsRefreshPeriod.toString());
setFacesParameter("jakarta.faces.VALIDATE_EMPTY_FIELDS", validateEmptyFields.toString()); setFacesParameter("jakarta.faces.VALIDATE_EMPTY_FIELDS", validateEmptyFields.toString());
setFacesParameter("jakarta.faces.FACELETS_SKIP_COMMENTS", skipComments.toString()); setFacesParameter("jakarta.faces.FACELETS_SKIP_COMMENTS", skipComments.toString());
} }
/** /**
* Configure les paramètres spécifiques au gestionnaire de vue. * Configure les paramètres spécifiques au gestionnaire de vue.
*/ */
private void configureViewHandler() { private void configureViewHandler() {
log.debug("Configuration du gestionnaire de vues JSF"); log.debug("Configuration du gestionnaire de vues JSF");
FacesContext facesContext = FacesContext.getCurrentInstance(); FacesContext facesContext = FacesContext.getCurrentInstance();
if (facesContext == null) { if (facesContext == null) {
log.warn("Impossible de configurer le gestionnaire de vue : FacesContext est null."); log.warn("Impossible de configurer le gestionnaire de vue : FacesContext est null.");
return; return;
} }
ViewHandler viewHandler = facesContext.getApplication().getViewHandler(); ViewHandler viewHandler = facesContext.getApplication().getViewHandler();
UIViewRoot root = facesContext.getViewRoot(); UIViewRoot root = facesContext.getViewRoot();
if (root == null) { if (root == null) {
root = viewHandler.createView(facesContext, "/index.xhtml"); root = viewHandler.createView(facesContext, "/index.xhtml");
facesContext.setViewRoot(root); facesContext.setViewRoot(root);
} }
root.getAttributes().put("encoding", "UTF-8"); root.getAttributes().put("encoding", "UTF-8");
root.getAttributes().put("contentType", "text/html"); root.getAttributes().put("contentType", "text/html");
root.getAttributes().put("characterEncoding", "UTF-8"); root.getAttributes().put("characterEncoding", "UTF-8");
log.debug("Gestionnaire de vues configuré avec succès."); log.debug("Gestionnaire de vues configuré avec succès.");
} }
/** /**
* Configure les paramètres spécifiques à PrimeFaces. * Configure les paramètres spécifiques à PrimeFaces.
*/ */
private void configurePrimeFaces() { private void configurePrimeFaces() {
log.debug("Configuration de PrimeFaces"); log.debug("Configuration de PrimeFaces");
setFacesParameter("primefaces.THEME", primefacesTheme); setFacesParameter("primefaces.THEME", primefacesTheme);
setFacesParameter("primefaces.FONT_AWESOME", "true"); setFacesParameter("primefaces.FONT_AWESOME", "true");
setFacesParameter("primefaces.CLIENT_SIDE_VALIDATION", "true"); setFacesParameter("primefaces.CLIENT_SIDE_VALIDATION", "true");
setFacesParameter("primefaces.UPLOADER", "auto"); setFacesParameter("primefaces.UPLOADER", "auto");
configurePrimeFacesCache(); configurePrimeFacesCache();
} }
/** /**
* Configure le cache PrimeFaces selon l'environnement. * Configure le cache PrimeFaces selon l'environnement.
*/ */
private void configurePrimeFacesCache() { private void configurePrimeFacesCache() {
String cacheProvider = isDevelopmentMode() ? "memory" : "ehcache"; String cacheProvider = isDevelopmentMode() ? "memory" : "ehcache";
setFacesParameter("primefaces.CACHE_PROVIDER", cacheProvider); setFacesParameter("primefaces.CACHE_PROVIDER", cacheProvider);
} }
/** /**
* Applique les optimisations de performance. * Applique les optimisations de performance.
*/ */
private void applyPerformanceOptimizations() { private void applyPerformanceOptimizations() {
if (!isDevelopmentMode()) { if (!isDevelopmentMode()) {
setFacesParameter("jakarta.faces.FACELETS_REFRESH_PERIOD", "-1"); setFacesParameter("jakarta.faces.FACELETS_REFRESH_PERIOD", "-1");
setFacesParameter("jakarta.faces.COMPRESS_VIEWSTATE", "true"); setFacesParameter("jakarta.faces.COMPRESS_VIEWSTATE", "true");
setFacesParameter("jakarta.faces.PARTIAL_STATE_SAVING", "true"); setFacesParameter("jakarta.faces.PARTIAL_STATE_SAVING", "true");
} }
} }
/** /**
* Définit un paramètre dans le contexte Faces. * Définit un paramètre dans le contexte Faces.
*/ */
private void setFacesParameter(String name, String value) { private void setFacesParameter(String name, String value) {
FacesContext.getCurrentInstance() FacesContext.getCurrentInstance()
.getExternalContext() .getExternalContext()
.getApplicationMap() .getApplicationMap()
.put(name, value); .put(name, value);
log.debug("Paramètre défini : {} = {}", name, value); log.debug("Paramètre défini : {} = {}", name, value);
} }
/** /**
* Vérifie si l'application est en mode développement. * Vérifie si l'application est en mode développement.
* *
* @return true si en mode développement, false sinon. * @return true si en mode développement, false sinon.
*/ */
public boolean isDevelopmentMode() { public boolean isDevelopmentMode() {
return "Development".equals(projectStage); return "Development".equals(projectStage);
} }
} }

View File

@@ -1,264 +1,264 @@
package dev.lions.config; package dev.lions.config;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event; import jakarta.enterprise.event.Event;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import dev.lions.events.StorageEvent; import dev.lions.events.StorageEvent;
import dev.lions.exceptions.StorageConfigurationException; import dev.lions.exceptions.StorageConfigurationException;
import dev.lions.utils.SecurityUtils; import dev.lions.utils.SecurityUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions; import java.nio.file.attribute.PosixFilePermissions;
import java.time.Duration; import java.time.Duration;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* Service de gestion avancée des configurations de stockage. * Service de gestion avancée des configurations de stockage.
* Assure la gestion sécurisée et optimisée des paramètres de stockage fichier * Assure la gestion sécurisée et optimisée des paramètres de stockage fichier
* de l'application, avec validation complète et monitoring. * de l'application, avec validation complète et monitoring.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 2.1 * @version 2.1
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class StorageConfigService { public class StorageConfigService {
private static final String DEFAULT_DIRECTORY_PERMISSIONS = "rwxr-x---"; private static final String DEFAULT_DIRECTORY_PERMISSIONS = "rwxr-x---";
private static final long MIN_FREE_SPACE_BYTES = 100 * 1024 * 1024; // 100 MB private static final long MIN_FREE_SPACE_BYTES = 100 * 1024 * 1024; // 100 MB
private static final int CLEANUP_BATCH_SIZE = 100; private static final int CLEANUP_BATCH_SIZE = 100;
private static final Duration CLEANUP_INTERVAL = Duration.ofHours(24); private static final Duration CLEANUP_INTERVAL = Duration.ofHours(24);
private final Map<String, Path> pathCache; private final Map<String, Path> pathCache;
private final Map<String, Boolean> fileTypeCache; private final Map<String, Boolean> fileTypeCache;
private final ApplicationConfig appConfig; private final ApplicationConfig appConfig;
private final SecurityUtils securityUtils; private final SecurityUtils securityUtils;
@Inject @Inject
Event<StorageEvent> storageEvent; Event<StorageEvent> storageEvent;
/** /**
* Initialise le service avec la configuration de l'application. * Initialise le service avec la configuration de l'application.
* *
* @param appConfig Configuration de l'application * @param appConfig Configuration de l'application
* @param securityUtils Utilitaires de sécurité * @param securityUtils Utilitaires de sécurité
*/ */
@Inject @Inject
public StorageConfigService(@NotNull ApplicationConfig appConfig, public StorageConfigService(@NotNull ApplicationConfig appConfig,
@NotNull SecurityUtils securityUtils) { @NotNull SecurityUtils securityUtils) {
this.appConfig = appConfig; this.appConfig = appConfig;
this.securityUtils = securityUtils; this.securityUtils = securityUtils;
this.pathCache = new ConcurrentHashMap<>(); this.pathCache = new ConcurrentHashMap<>();
this.fileTypeCache = new ConcurrentHashMap<>(); this.fileTypeCache = new ConcurrentHashMap<>();
initializeStorage(); initializeStorage();
} }
/** /**
* Initialise et valide la configuration du stockage. * Initialise et valide la configuration du stockage.
*/ */
private void initializeStorage() { private void initializeStorage() {
log.info("Initialisation de la configuration du stockage"); log.info("Initialisation de la configuration du stockage");
try { try {
createMainDirectories(); createMainDirectories();
configureSecurity(); configureSecurity();
scheduleMaintenanceTasks(); scheduleMaintenanceTasks();
validateStorageCapacity(); validateStorageCapacity();
notifyStorageInitialized(); notifyStorageInitialized();
log.info("Configuration du stockage initialisée avec succès"); log.info("Configuration du stockage initialisée avec succès");
} catch (Exception e) { } catch (Exception e) {
String message = "Erreur lors de l'initialisation du stockage"; String message = "Erreur lors de l'initialisation du stockage";
log.error(message, e); log.error(message, e);
throw new StorageConfigurationException(message, e); throw new StorageConfigurationException(message, e);
} }
} }
/** /**
* Crée les répertoires principaux nécessaires au stockage. * Crée les répertoires principaux nécessaires au stockage.
*/ */
private void createMainDirectories() { private void createMainDirectories() {
log.debug("Création des répertoires principaux de stockage"); log.debug("Création des répertoires principaux de stockage");
String basePath = appConfig.getStorageBasePath(); String basePath = appConfig.getStorageBasePath();
Set<String> requiredDirs = Set.of("images", "documents", "temp", "backup"); Set<String> requiredDirs = Set.of("images", "documents", "temp", "backup");
requiredDirs.forEach(dir -> { requiredDirs.forEach(dir -> {
Path dirPath = Paths.get(basePath, dir); Path dirPath = Paths.get(basePath, dir);
createSecureDirectory(dirPath); createSecureDirectory(dirPath);
}); });
log.info("Répertoires principaux créés avec succès."); log.info("Répertoires principaux créés avec succès.");
} }
/** /**
* Configure les paramètres de sécurité pour la production. * Configure les paramètres de sécurité pour la production.
*/ */
private void applyProductionSecurity() { private void applyProductionSecurity() {
log.info("Application des paramètres de sécurité spécifiques pour la production"); log.info("Application des paramètres de sécurité spécifiques pour la production");
try { try {
Path basePath = Paths.get(appConfig.getStorageBasePath()); Path basePath = Paths.get(appConfig.getStorageBasePath());
// Applique des permissions POSIX sécurisées // Applique des permissions POSIX sécurisées
Set<PosixFilePermission> permissions = Set<PosixFilePermission> permissions =
PosixFilePermissions.fromString(DEFAULT_DIRECTORY_PERMISSIONS); PosixFilePermissions.fromString(DEFAULT_DIRECTORY_PERMISSIONS);
Files.setPosixFilePermissions(basePath, permissions); Files.setPosixFilePermissions(basePath, permissions);
log.info("Permissions POSIX sécurisées appliquées : {}", permissions); log.info("Permissions POSIX sécurisées appliquées : {}", permissions);
// Vérifie l'accès sécurisé // Vérifie l'accès sécurisé
if (!Files.isWritable(basePath) || !Files.isReadable(basePath)) { if (!Files.isWritable(basePath) || !Files.isReadable(basePath)) {
throw new StorageConfigurationException( throw new StorageConfigurationException(
"Permissions insuffisantes sur le répertoire de stockage : " + basePath); "Permissions insuffisantes sur le répertoire de stockage : " + basePath);
} }
log.debug("Sécurité des répertoires en production validée"); log.debug("Sécurité des répertoires en production validée");
} catch (IOException e) { } catch (IOException e) {
log.error("Erreur lors de l'application des permissions de sécurité", e); log.error("Erreur lors de l'application des permissions de sécurité", e);
throw new StorageConfigurationException( throw new StorageConfigurationException(
"Impossible d'appliquer les paramètres de sécurité en production", e); "Impossible d'appliquer les paramètres de sécurité en production", e);
} }
} }
/** /**
* Configure les paramètres de sécurité pour le stockage. * Configure les paramètres de sécurité pour le stockage.
*/ */
private void configureSecurity() { private void configureSecurity() {
log.info("Configuration des paramètres de sécurité du stockage"); log.info("Configuration des paramètres de sécurité du stockage");
try { try {
if (appConfig.isProduction()) { if (appConfig.isProduction()) {
applyProductionSecurity(); applyProductionSecurity();
} }
securityUtils.initializeEncryption(); securityUtils.initializeEncryption();
log.info("Sécurité du stockage configurée avec succès"); log.info("Sécurité du stockage configurée avec succès");
} catch (Exception e) { } catch (Exception e) {
throw new StorageConfigurationException("Erreur lors de la configuration de la sécurité", e); throw new StorageConfigurationException("Erreur lors de la configuration de la sécurité", e);
} }
} }
/** /**
* Planifie les tâches de maintenance pour le stockage. * Planifie les tâches de maintenance pour le stockage.
*/ */
private void scheduleMaintenanceTasks() { private void scheduleMaintenanceTasks() {
log.info("Planification des tâches de maintenance du stockage"); log.info("Planification des tâches de maintenance du stockage");
try { try {
scheduleStorageCleanup(); scheduleStorageCleanup();
scheduleCapacityCheck(); scheduleCapacityCheck();
log.info("Tâches de maintenance planifiées avec succès."); log.info("Tâches de maintenance planifiées avec succès.");
} catch (Exception e) { } catch (Exception e) {
throw new StorageConfigurationException("Erreur lors de la planification des tâches de maintenance", e); throw new StorageConfigurationException("Erreur lors de la planification des tâches de maintenance", e);
} }
} }
/** /**
* Crée un répertoire sécurisé avec les permissions appropriées. * Crée un répertoire sécurisé avec les permissions appropriées.
*/ */
private void createSecureDirectory(Path path) { private void createSecureDirectory(Path path) {
try { try {
if (!Files.exists(path)) { if (!Files.exists(path)) {
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions
.asFileAttribute(PosixFilePermissions.fromString(DEFAULT_DIRECTORY_PERMISSIONS)); .asFileAttribute(PosixFilePermissions.fromString(DEFAULT_DIRECTORY_PERMISSIONS));
Files.createDirectories(path, attr); Files.createDirectories(path, attr);
log.debug("Répertoire créé avec succès : {}", path); log.debug("Répertoire créé avec succès : {}", path);
} }
validateDirectoryAccess(path); validateDirectoryAccess(path);
} catch (IOException e) { } catch (IOException e) {
throw new StorageConfigurationException( throw new StorageConfigurationException(
"Impossible de créer le répertoire sécurisé : " + path, e); "Impossible de créer le répertoire sécurisé : " + path, e);
} }
} }
/** /**
* Planifie le nettoyage automatique du stockage. * Planifie le nettoyage automatique du stockage.
*/ */
private void scheduleStorageCleanup() { private void scheduleStorageCleanup() {
log.info("Planification de la tâche de nettoyage automatique du stockage"); log.info("Planification de la tâche de nettoyage automatique du stockage");
// Simulation d'une tâche de nettoyage. Remplacer par un vrai scheduler si nécessaire. // Simulation d'une tâche de nettoyage. Remplacer par un vrai scheduler si nécessaire.
log.debug("Nettoyage automatique exécuté toutes les {} heures, batch size: {}", log.debug("Nettoyage automatique exécuté toutes les {} heures, batch size: {}",
CLEANUP_INTERVAL.toHours(), CLEANUP_BATCH_SIZE); CLEANUP_INTERVAL.toHours(), CLEANUP_BATCH_SIZE);
} }
/** /**
* Planifie la vérification périodique de la capacité de stockage. * Planifie la vérification périodique de la capacité de stockage.
*/ */
private void scheduleCapacityCheck() { private void scheduleCapacityCheck() {
log.info("Planification de la vérification périodique de la capacité de stockage"); log.info("Planification de la vérification périodique de la capacité de stockage");
// Simulation d'une vérification périodique. À remplacer par un scheduler réel. // Simulation d'une vérification périodique. À remplacer par un scheduler réel.
log.debug("Vérification de la capacité planifiée toutes les {} heures", CLEANUP_INTERVAL.toHours()); log.debug("Vérification de la capacité planifiée toutes les {} heures", CLEANUP_INTERVAL.toHours());
} }
/** /**
* Valide la capacité de stockage disponible. * Valide la capacité de stockage disponible.
*/ */
private void validateStorageCapacity() { private void validateStorageCapacity() {
try { try {
Path storagePath = Paths.get(appConfig.getStorageBasePath()); Path storagePath = Paths.get(appConfig.getStorageBasePath());
long freeSpace = Files.getFileStore(storagePath).getUsableSpace(); long freeSpace = Files.getFileStore(storagePath).getUsableSpace();
if (freeSpace < MIN_FREE_SPACE_BYTES) { if (freeSpace < MIN_FREE_SPACE_BYTES) {
throw new StorageConfigurationException( throw new StorageConfigurationException(
"Espace de stockage insuffisant. Minimum requis : " + "Espace de stockage insuffisant. Minimum requis : " +
formatSize(MIN_FREE_SPACE_BYTES)); formatSize(MIN_FREE_SPACE_BYTES));
} }
log.debug("Espace de stockage validé : {} disponible", formatSize(freeSpace)); log.debug("Espace de stockage validé : {} disponible", formatSize(freeSpace));
} catch (IOException e) { } catch (IOException e) {
throw new StorageConfigurationException( throw new StorageConfigurationException(
"Impossible de vérifier l'espace de stockage disponible", e); "Impossible de vérifier l'espace de stockage disponible", e);
} }
} }
/** /**
* Formate une taille en bytes en format lisible. * Formate une taille en bytes en format lisible.
*/ */
private String formatSize(long bytes) { private String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B"; if (bytes < 1024) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(1024)); int exp = (int) (Math.log(bytes) / Math.log(1024));
String pre = "KMGTPE".charAt(exp - 1) + ""; String pre = "KMGTPE".charAt(exp - 1) + "";
return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre); return String.format("%.1f %sB", bytes / Math.pow(1024, exp), pre);
} }
/** /**
* Notifie les observateurs de l'initialisation du stockage. * Notifie les observateurs de l'initialisation du stockage.
*/ */
private void notifyStorageInitialized() { private void notifyStorageInitialized() {
StorageEvent event = new StorageEvent( StorageEvent event = new StorageEvent(
"STORAGE_INITIALIZED", "STORAGE_INITIALIZED",
Map.of( Map.of(
"basePath", appConfig.getStorageBasePath(), "basePath", appConfig.getStorageBasePath(),
"environment", appConfig.getEnvironment() "environment", appConfig.getEnvironment()
) )
); );
storageEvent.fire(event); storageEvent.fire(event);
} }
/** /**
* Valide l'accès au répertoire. * Valide l'accès au répertoire.
*/ */
private void validateDirectoryAccess(Path path) { private void validateDirectoryAccess(Path path) {
if (!Files.isDirectory(path)) { if (!Files.isDirectory(path)) {
throw new StorageConfigurationException( throw new StorageConfigurationException(
"Le chemin n'est pas un répertoire : " + path); "Le chemin n'est pas un répertoire : " + path);
} }
if (!Files.isWritable(path)) { if (!Files.isWritable(path)) {
throw new StorageConfigurationException( throw new StorageConfigurationException(
"Le répertoire n'est pas accessible en écriture : " + path); "Le répertoire n'est pas accessible en écriture : " + path);
} }
} }
} }

View File

@@ -1,52 +1,52 @@
package dev.lions.dtos; package dev.lions.dtos;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import dev.lions.models.Notification; import dev.lions.models.Notification;
import dev.lions.models.NotificationStatus; import dev.lions.models.NotificationStatus;
import dev.lions.models.NotificationType; import dev.lions.models.NotificationType;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Data @Data
@Builder @Builder
@Slf4j @Slf4j
public class NotificationDTO { public class NotificationDTO {
private Long id; private Long id;
private String title; private String title;
private String message; private String message;
private NotificationType type; private NotificationType type;
private NotificationStatus status; private NotificationStatus status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime timestamp; private LocalDateTime timestamp;
private String actionUrl; private String actionUrl;
private Long targetUserId; private Long targetUserId;
private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectMapper objectMapper = new ObjectMapper();
public static NotificationDTO from(Notification notification) { public static NotificationDTO from(Notification notification) {
return NotificationDTO.builder() return NotificationDTO.builder()
.id(notification.getId()) .id(notification.getId())
.title(notification.getTitle()) .title(notification.getTitle())
.message(notification.getMessage()) .message(notification.getMessage())
.type(notification.getType()) .type(notification.getType())
.status(notification.getStatus()) .status(notification.getStatus())
.timestamp(notification.getTimestamp()) .timestamp(notification.getTimestamp())
.actionUrl(notification.getActionUrl()) .actionUrl(notification.getActionUrl())
.targetUserId(notification.getTargetUserId()) .targetUserId(notification.getTargetUserId())
.build(); .build();
} }
public String toJson() { public String toJson() {
try { try {
return objectMapper.writeValueAsString(this); return objectMapper.writeValueAsString(this);
} catch (Exception e) { } catch (Exception e) {
log.error("Error converting notification to JSON", e); log.error("Error converting notification to JSON", e);
return String.format("{\"error\":\"Failed to serialize notification %d\"}", id); return String.format("{\"error\":\"Failed to serialize notification %d\"}", id);
} }
} }
} }

View File

@@ -0,0 +1,197 @@
package dev.lions.erp.core;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.ArrayList;
/**
* Entité Entreprise - Multi-tenant
*/
@Entity
@Table(name = "companies")
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String tenantId; // Identifiant unique pour multi-tenant
@Column(nullable = false)
private String name;
private String legalName;
private String registrationNumber; // Numéro RCCM
private String taxNumber; // Numéro contribuable DGI
private String cnpsNumber; // Numéro CNPS
// Adresse
private String address;
private String city;
private String postalCode;
private String country = "Côte d'Ivoire";
// Contact
private String phone;
private String email;
private String website;
// Informations métier
private String sector;
private String activity;
private Integer employeeCount;
private String currency = "FCFA";
// Configuration
@Enumerated(EnumType.STRING)
private CompanyStatus status = CompanyStatus.ACTIVE;
@ElementCollection
@Enumerated(EnumType.STRING)
@CollectionTable(name = "company_modules")
private List<ModuleType> enabledModules = new ArrayList<>();
// Dates
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime lastLoginAt;
// Constructeurs
public Company() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public Company(String name, String tenantId) {
this();
this.name = name;
this.tenantId = tenantId;
}
// Méthodes métier
public boolean hasModule(ModuleType moduleType) {
return enabledModules.contains(moduleType);
}
public void enableModule(ModuleType moduleType) {
if (!enabledModules.contains(moduleType)) {
enabledModules.add(moduleType);
this.updatedAt = LocalDateTime.now();
}
}
public void disableModule(ModuleType moduleType) {
if (enabledModules.remove(moduleType)) {
this.updatedAt = LocalDateTime.now();
}
}
public boolean isActive() {
return status == CompanyStatus.ACTIVE;
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTenantId() { return tenantId; }
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getLegalName() { return legalName; }
public void setLegalName(String legalName) { this.legalName = legalName; }
public String getRegistrationNumber() { return registrationNumber; }
public void setRegistrationNumber(String registrationNumber) { this.registrationNumber = registrationNumber; }
public String getTaxNumber() { return taxNumber; }
public void setTaxNumber(String taxNumber) { this.taxNumber = taxNumber; }
public String getCnpsNumber() { return cnpsNumber; }
public void setCnpsNumber(String cnpsNumber) { this.cnpsNumber = cnpsNumber; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
public String getPostalCode() { return postalCode; }
public void setPostalCode(String postalCode) { this.postalCode = postalCode; }
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getWebsite() { return website; }
public void setWebsite(String website) { this.website = website; }
public String getSector() { return sector; }
public void setSector(String sector) { this.sector = sector; }
public String getActivity() { return activity; }
public void setActivity(String activity) { this.activity = activity; }
public Integer getEmployeeCount() { return employeeCount; }
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
public CompanyStatus getStatus() { return status; }
public void setStatus(CompanyStatus status) { this.status = status; }
public List<ModuleType> getEnabledModules() { return enabledModules; }
public void setEnabledModules(List<ModuleType> enabledModules) { this.enabledModules = enabledModules; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; }
}
/**
* Statut de l'entreprise
*/
enum CompanyStatus {
ACTIVE, // Active
SUSPENDED, // Suspendue
TRIAL, // En période d'essai
EXPIRED // Expirée
}
/**
* Types de modules ERP
*/
enum ModuleType {
CRM, // Gestion commerciale
STOCK, // Gestion des stocks
ACCOUNTING, // Comptabilité
HR, // Ressources humaines
PROJECT, // Gestion de projets
PURCHASE, // Achats
SALES, // Ventes
INVENTORY, // Inventaire
REPORTING, // Rapports
DASHBOARD // Tableaux de bord
}

View File

@@ -0,0 +1,338 @@
package dev.lions.erp.core;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import java.util.UUID;
import java.util.List;
import java.util.Optional;
/**
* Service de gestion multi-tenant
*/
@ApplicationScoped
public class TenantService {
@Inject
EntityManager em;
/**
* Crée une nouvelle entreprise (tenant)
*/
@Transactional
public Company createCompany(String name, String email, String contactName) {
// Génération d'un tenant ID unique
String tenantId = generateTenantId();
// Création de l'entreprise
Company company = new Company(name, tenantId);
company.setEmail(email);
// Modules par défaut
company.enableModule(ModuleType.CRM);
company.enableModule(ModuleType.DASHBOARD);
em.persist(company);
// Création de l'utilisateur administrateur
User admin = createAdminUser(contactName, email, tenantId);
return company;
}
/**
* Crée l'utilisateur administrateur pour une entreprise
*/
@Transactional
public User createAdminUser(String fullName, String email, String tenantId) {
String[] names = fullName.split(" ", 2);
String firstName = names[0];
String lastName = names.length > 1 ? names[1] : "";
User admin = new User(email, firstName, lastName, tenantId);
admin.addRole(UserRole.ADMIN);
admin.setPosition("Administrateur");
admin.setEmailVerified(true);
// Permissions complètes
admin.addPermission(Permission.USER_MANAGEMENT);
admin.addPermission(Permission.COMPANY_SETTINGS);
admin.addPermission(Permission.CRM_READ);
admin.addPermission(Permission.CRM_WRITE);
admin.addPermission(Permission.REPORTS_VIEW);
// Mot de passe temporaire (à changer au premier login)
admin.setPasswordHash(hashPassword("TempPass123!"));
em.persist(admin);
return admin;
}
/**
* Récupère une entreprise par tenant ID
*/
public Optional<Company> getCompanyByTenantId(String tenantId) {
try {
Company company = em.createQuery(
"SELECT c FROM Company c WHERE c.tenantId = :tenantId", Company.class)
.setParameter("tenantId", tenantId)
.getSingleResult();
return Optional.of(company);
} catch (Exception e) {
return Optional.empty();
}
}
/**
* Récupère un utilisateur par email et tenant
*/
public Optional<User> getUserByEmailAndTenant(String email, String tenantId) {
try {
User user = em.createQuery(
"SELECT u FROM User u WHERE u.email = :email AND u.tenantId = :tenantId", User.class)
.setParameter("email", email)
.setParameter("tenantId", tenantId)
.getSingleResult();
return Optional.of(user);
} catch (Exception e) {
return Optional.empty();
}
}
/**
* Liste les utilisateurs d'une entreprise
*/
public List<User> getUsersByTenant(String tenantId) {
return em.createQuery(
"SELECT u FROM User u WHERE u.tenantId = :tenantId ORDER BY u.firstName, u.lastName", User.class)
.setParameter("tenantId", tenantId)
.getResultList();
}
/**
* Active un module pour une entreprise
*/
@Transactional
public void enableModule(String tenantId, ModuleType moduleType) {
Company company = getCompanyByTenantId(tenantId)
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
company.enableModule(moduleType);
}
/**
* Désactive un module pour une entreprise
*/
@Transactional
public void disableModule(String tenantId, ModuleType moduleType) {
Company company = getCompanyByTenantId(tenantId)
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
company.disableModule(moduleType);
}
/**
* Vérifie si une entreprise a accès à un module
*/
public boolean hasModuleAccess(String tenantId, ModuleType moduleType) {
return getCompanyByTenantId(tenantId)
.map(company -> company.hasModule(moduleType))
.orElse(false);
}
/**
* Met à jour les informations d'une entreprise
*/
@Transactional
public Company updateCompany(String tenantId, CompanyUpdateDTO updateData) {
Company company = getCompanyByTenantId(tenantId)
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
if (updateData.getName() != null) {
company.setName(updateData.getName());
}
if (updateData.getAddress() != null) {
company.setAddress(updateData.getAddress());
}
if (updateData.getPhone() != null) {
company.setPhone(updateData.getPhone());
}
if (updateData.getEmail() != null) {
company.setEmail(updateData.getEmail());
}
if (updateData.getSector() != null) {
company.setSector(updateData.getSector());
}
if (updateData.getEmployeeCount() != null) {
company.setEmployeeCount(updateData.getEmployeeCount());
}
return company;
}
/**
* Crée un nouvel utilisateur dans une entreprise
*/
@Transactional
public User createUser(String tenantId, UserCreateDTO userData) {
// Vérification que l'entreprise existe
getCompanyByTenantId(tenantId)
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
// Vérification que l'email n'existe pas déjà
if (getUserByEmailAndTenant(userData.getEmail(), tenantId).isPresent()) {
throw new IllegalArgumentException("Un utilisateur avec cet email existe déjà");
}
User user = new User(userData.getEmail(), userData.getFirstName(),
userData.getLastName(), tenantId);
user.setPhone(userData.getPhone());
user.setPosition(userData.getPosition());
user.setDepartment(userData.getDepartment());
// Rôle par défaut
user.addRole(UserRole.USER);
// Permissions de base
user.addPermission(Permission.CRM_READ);
user.addPermission(Permission.REPORTS_VIEW);
// Mot de passe temporaire
user.setPasswordHash(hashPassword("TempPass123!"));
em.persist(user);
return user;
}
/**
* Supprime une entreprise et tous ses utilisateurs
*/
@Transactional
public void deleteCompany(String tenantId) {
// Suppression des utilisateurs
em.createQuery("DELETE FROM User u WHERE u.tenantId = :tenantId")
.setParameter("tenantId", tenantId)
.executeUpdate();
// Suppression de l'entreprise
em.createQuery("DELETE FROM Company c WHERE c.tenantId = :tenantId")
.setParameter("tenantId", tenantId)
.executeUpdate();
}
/**
* Statistiques d'une entreprise
*/
public TenantStats getTenantStats(String tenantId) {
Company company = getCompanyByTenantId(tenantId)
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
Long userCount = em.createQuery(
"SELECT COUNT(u) FROM User u WHERE u.tenantId = :tenantId", Long.class)
.setParameter("tenantId", tenantId)
.getSingleResult();
Long activeUserCount = em.createQuery(
"SELECT COUNT(u) FROM User u WHERE u.tenantId = :tenantId AND u.status = :status", Long.class)
.setParameter("tenantId", tenantId)
.setParameter("status", UserStatus.ACTIVE)
.getSingleResult();
return new TenantStats(company, userCount, activeUserCount);
}
// Méthodes utilitaires
private String generateTenantId() {
return "tenant_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
}
private String hashPassword(String password) {
// Implémentation simple - en production, utiliser BCrypt ou Argon2
return "hashed_" + password;
}
}
/**
* DTO pour mise à jour entreprise
*/
class CompanyUpdateDTO {
private String name;
private String address;
private String phone;
private String email;
private String sector;
private Integer employeeCount;
// Getters et Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getSector() { return sector; }
public void setSector(String sector) { this.sector = sector; }
public Integer getEmployeeCount() { return employeeCount; }
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
}
/**
* DTO pour création utilisateur
*/
class UserCreateDTO {
private String email;
private String firstName;
private String lastName;
private String phone;
private String position;
private String department;
// Getters et Setters
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getPosition() { return position; }
public void setPosition(String position) { this.position = position; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
}
/**
* Statistiques tenant
*/
class TenantStats {
private Company company;
private Long totalUsers;
private Long activeUsers;
public TenantStats(Company company, Long totalUsers, Long activeUsers) {
this.company = company;
this.totalUsers = totalUsers;
this.activeUsers = activeUsers;
}
// Getters
public Company getCompany() { return company; }
public Long getTotalUsers() { return totalUsers; }
public Long getActiveUsers() { return activeUsers; }
}

View File

@@ -0,0 +1,257 @@
package dev.lions.erp.core;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.HashSet;
/**
* Entité Utilisateur - Multi-tenant
*/
@Entity
@Table(name = "users", uniqueConstraints = {
@UniqueConstraint(columnNames = {"email", "tenantId"})
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String tenantId; // Lien vers l'entreprise
@Column(nullable = false)
private String email;
@Column(nullable = false)
private String firstName;
@Column(nullable = false)
private String lastName;
private String phone;
private String position; // Poste dans l'entreprise
private String department;
// Authentification
@Column(nullable = false)
private String passwordHash;
private String resetToken;
private LocalDateTime resetTokenExpiry;
// Permissions
@ElementCollection
@Enumerated(EnumType.STRING)
@CollectionTable(name = "user_roles")
private Set<UserRole> roles = new HashSet<>();
@ElementCollection
@Enumerated(EnumType.STRING)
@CollectionTable(name = "user_permissions")
private Set<Permission> permissions = new HashSet<>();
// Statut
@Enumerated(EnumType.STRING)
private UserStatus status = UserStatus.ACTIVE;
private Boolean emailVerified = false;
private String emailVerificationToken;
// Dates
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime lastLoginAt;
private LocalDateTime lastActivityAt;
// Préférences
private String language = "fr";
private String timezone = "Africa/Abidjan";
private String theme = "light";
// Constructeurs
public User() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
public User(String email, String firstName, String lastName, String tenantId) {
this();
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
this.tenantId = tenantId;
}
// Méthodes métier
public String getFullName() {
return firstName + " " + lastName;
}
public boolean hasRole(UserRole role) {
return roles.contains(role);
}
public boolean hasPermission(Permission permission) {
return permissions.contains(permission) || hasAdminRole();
}
public boolean hasAdminRole() {
return roles.contains(UserRole.ADMIN) || roles.contains(UserRole.SUPER_ADMIN);
}
public void addRole(UserRole role) {
roles.add(role);
this.updatedAt = LocalDateTime.now();
}
public void removeRole(UserRole role) {
roles.remove(role);
this.updatedAt = LocalDateTime.now();
}
public void addPermission(Permission permission) {
permissions.add(permission);
this.updatedAt = LocalDateTime.now();
}
public void removePermission(Permission permission) {
permissions.remove(permission);
this.updatedAt = LocalDateTime.now();
}
public boolean isActive() {
return status == UserStatus.ACTIVE;
}
public void updateLastActivity() {
this.lastActivityAt = LocalDateTime.now();
}
public void updateLastLogin() {
this.lastLoginAt = LocalDateTime.now();
this.lastActivityAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTenantId() { return tenantId; }
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getPosition() { return position; }
public void setPosition(String position) { this.position = position; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
public String getResetToken() { return resetToken; }
public void setResetToken(String resetToken) { this.resetToken = resetToken; }
public LocalDateTime getResetTokenExpiry() { return resetTokenExpiry; }
public void setResetTokenExpiry(LocalDateTime resetTokenExpiry) { this.resetTokenExpiry = resetTokenExpiry; }
public Set<UserRole> getRoles() { return roles; }
public void setRoles(Set<UserRole> roles) { this.roles = roles; }
public Set<Permission> getPermissions() { return permissions; }
public void setPermissions(Set<Permission> permissions) { this.permissions = permissions; }
public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }
public Boolean getEmailVerified() { return emailVerified; }
public void setEmailVerified(Boolean emailVerified) { this.emailVerified = emailVerified; }
public String getEmailVerificationToken() { return emailVerificationToken; }
public void setEmailVerificationToken(String emailVerificationToken) { this.emailVerificationToken = emailVerificationToken; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; }
public LocalDateTime getLastActivityAt() { return lastActivityAt; }
public void setLastActivityAt(LocalDateTime lastActivityAt) { this.lastActivityAt = lastActivityAt; }
public String getLanguage() { return language; }
public void setLanguage(String language) { this.language = language; }
public String getTimezone() { return timezone; }
public void setTimezone(String timezone) { this.timezone = timezone; }
public String getTheme() { return theme; }
public void setTheme(String theme) { this.theme = theme; }
}
/**
* Rôles utilisateur
*/
enum UserRole {
SUPER_ADMIN, // Super administrateur Lions Dev
ADMIN, // Administrateur entreprise
MANAGER, // Manager/Responsable
USER, // Utilisateur standard
VIEWER // Consultation uniquement
}
/**
* Permissions spécifiques
*/
enum Permission {
// CRM
CRM_READ, CRM_WRITE, CRM_DELETE,
// Stock
STOCK_READ, STOCK_WRITE, STOCK_DELETE,
// Comptabilité
ACCOUNTING_READ, ACCOUNTING_WRITE, ACCOUNTING_DELETE,
// RH
HR_READ, HR_WRITE, HR_DELETE,
// Administration
USER_MANAGEMENT, COMPANY_SETTINGS, SYSTEM_CONFIG,
// Rapports
REPORTS_VIEW, REPORTS_EXPORT, REPORTS_CREATE
}
/**
* Statut utilisateur
*/
enum UserStatus {
ACTIVE, // Actif
INACTIVE, // Inactif
SUSPENDED, // Suspendu
PENDING_VERIFICATION // En attente de vérification
}

View File

@@ -1,226 +1,226 @@
package dev.lions.events; package dev.lions.events;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.Convert; import jakarta.persistence.Convert;
import jakarta.persistence.Temporal; import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType; import jakarta.persistence.TemporalType;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import dev.lions.utils.JsonConverter; import dev.lions.utils.JsonConverter;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.io.Serializable; import java.io.Serializable;
/** /**
* Entité représentant un événement analytique dans le système. * Entité représentant un événement analytique dans le système.
* Cette classe permet de tracer et d'analyser les différentes actions et interactions * Cette classe permet de tracer et d'analyser les différentes actions et interactions
* des utilisateurs avec l'application. * des utilisateurs avec l'application.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.1 * @version 1.1
*/ */
@Slf4j @Slf4j
@Data @Data
@Entity @Entity
@Table(name = "analytics_events") @Table(name = "analytics_events")
@Builder(toBuilder = true) @Builder(toBuilder = true)
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Accessors(chain = true) @Accessors(chain = true)
public class AnalyticsEvent implements Serializable { public class AnalyticsEvent implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Identifiant unique de l'événement * Identifiant unique de l'événement
*/ */
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
/** /**
* Type d'événement analytique (ex: PAGE_VIEW, USER_ACTION, etc.) * Type d'événement analytique (ex: PAGE_VIEW, USER_ACTION, etc.)
*/ */
@NotNull(message = "Le type d'événement est obligatoire") @NotNull(message = "Le type d'événement est obligatoire")
@Column(name = "event_type", nullable = false) @Column(name = "event_type", nullable = false)
private String eventType; private String eventType;
/** /**
* Identifiant de l'utilisateur associé à l'événement * Identifiant de l'utilisateur associé à l'événement
*/ */
@Column(name = "user_id") @Column(name = "user_id")
private String userId; private String userId;
/** /**
* Identifiant du contact associé à l'événement * Identifiant du contact associé à l'événement
*/ */
@Column(name = "contact_id") @Column(name = "contact_id")
private String contactId; private String contactId;
/** /**
* Source de l'événement (ex: WEB, MOBILE, API) * Source de l'événement (ex: WEB, MOBILE, API)
*/ */
@Column(name = "source") @Column(name = "source")
private String source; private String source;
/** /**
* Propriétés additionnelles de l'événement stockées au format JSON * Propriétés additionnelles de l'événement stockées au format JSON
*/ */
@Convert(converter = JsonConverter.class) @Convert(converter = JsonConverter.class)
@Column(name = "properties", columnDefinition = "jsonb") @Column(name = "properties", columnDefinition = "jsonb")
@Builder.Default @Builder.Default
private Map<String, Object> properties = new HashMap<>(); private Map<String, Object> properties = new HashMap<>();
/** /**
* Date et heure de l'événement * Date et heure de l'événement
*/ */
@Temporal(TemporalType.TIMESTAMP) @Temporal(TemporalType.TIMESTAMP)
@Column(name = "timestamp", nullable = false) @Column(name = "timestamp", nullable = false)
@Builder.Default @Builder.Default
private LocalDateTime timestamp = LocalDateTime.now(); private LocalDateTime timestamp = LocalDateTime.now();
/** /**
* Environnement dans lequel l'événement s'est produit * Environnement dans lequel l'événement s'est produit
*/ */
@Column(name = "environment", nullable = false) @Column(name = "environment", nullable = false)
@Builder.Default @Builder.Default
private String environment = System.getProperty("app.environment", "production"); private String environment = System.getProperty("app.environment", "production");
/** /**
* Type d'événements analytiques supportés * Type d'événements analytiques supportés
*/ */
public enum EventType { public enum EventType {
PAGE_VIEW, PAGE_VIEW,
USER_ACTION, USER_ACTION,
SYSTEM_EVENT, SYSTEM_EVENT,
ERROR, ERROR,
PERFORMANCE, PERFORMANCE,
SECURITY SECURITY
} }
/** /**
* Crée une copie de l'événement avec des propriétés enrichies * Crée une copie de l'événement avec des propriétés enrichies
* *
* @param additionalProps Propriétés supplémentaires à ajouter * @param additionalProps Propriétés supplémentaires à ajouter
* @return Nouvelle instance d'AnalyticsEvent avec les propriétés enrichies * @return Nouvelle instance d'AnalyticsEvent avec les propriétés enrichies
*/ */
public AnalyticsEvent withAdditionalProperties(Map<String, Object> additionalProps) { public AnalyticsEvent withAdditionalProperties(Map<String, Object> additionalProps) {
if (additionalProps == null || additionalProps.isEmpty()) { if (additionalProps == null || additionalProps.isEmpty()) {
log.debug("Aucune propriété additionnelle à ajouter"); log.debug("Aucune propriété additionnelle à ajouter");
return this; return this;
} }
log.debug("Ajout de {} propriétés additionnelles à l'événement", additionalProps.size()); log.debug("Ajout de {} propriétés additionnelles à l'événement", additionalProps.size());
Map<String, Object> newProps = new HashMap<>(this.properties); Map<String, Object> newProps = new HashMap<>(this.properties);
newProps.putAll(additionalProps); newProps.putAll(additionalProps);
return this.toBuilder() return this.toBuilder()
.properties(newProps) .properties(newProps)
.build(); .build();
} }
/** /**
* Ajoute une propriété unique à l'événement * Ajoute une propriété unique à l'événement
* *
* @param key Clé de la propriété * @param key Clé de la propriété
* @param value Valeur de la propriété * @param value Valeur de la propriété
* @return L'instance actuelle pour chaînage * @return L'instance actuelle pour chaînage
*/ */
public AnalyticsEvent addProperty(String key, Object value) { public AnalyticsEvent addProperty(String key, Object value) {
if (key == null || key.trim().isEmpty()) { if (key == null || key.trim().isEmpty()) {
log.warn("Tentative d'ajout d'une propriété avec une clé nulle ou vide"); log.warn("Tentative d'ajout d'une propriété avec une clé nulle ou vide");
return this; return this;
} }
log.debug("Ajout de la propriété '{}' à l'événement", key); log.debug("Ajout de la propriété '{}' à l'événement", key);
this.properties.put(key, value); this.properties.put(key, value);
return this; return this;
} }
/** /**
* Enrichit l'événement avec des métadonnées standard * Enrichit l'événement avec des métadonnées standard
* *
* @return L'instance actuelle pour chaînage * @return L'instance actuelle pour chaînage
*/ */
public AnalyticsEvent enrichWithMetadata() { public AnalyticsEvent enrichWithMetadata() {
log.debug("Enrichissement de l'événement {} avec les métadonnées standard", this.id); log.debug("Enrichissement de l'événement {} avec les métadonnées standard", this.id);
this.addProperty("timestamp_ms", System.currentTimeMillis()) this.addProperty("timestamp_ms", System.currentTimeMillis())
.addProperty("java_version", System.getProperty("java.version")) .addProperty("java_version", System.getProperty("java.version"))
.addProperty("os_name", System.getProperty("os.name")) .addProperty("os_name", System.getProperty("os.name"))
.addProperty("app_version", System.getProperty("app.version")) .addProperty("app_version", System.getProperty("app.version"))
.addProperty("node_id", System.getProperty("node.id")) .addProperty("node_id", System.getProperty("node.id"))
.addProperty("thread_name", Thread.currentThread().getName()); .addProperty("thread_name", Thread.currentThread().getName());
return this; return this;
} }
/** /**
* Vérifie si l'événement est valide pour le traitement * Vérifie si l'événement est valide pour le traitement
*/ */
public boolean isValid() { public boolean isValid() {
boolean isValid = this.eventType != null && boolean isValid = this.eventType != null &&
!this.eventType.trim().isEmpty() && !this.eventType.trim().isEmpty() &&
this.timestamp != null; this.timestamp != null;
if (!isValid) { if (!isValid) {
log.warn("Événement invalide détecté: type={}, timestamp={}", log.warn("Événement invalide détecté: type={}, timestamp={}",
this.eventType, this.timestamp); this.eventType, this.timestamp);
} }
return isValid; return isValid;
} }
/** /**
* Marque l'événement comme ayant été traité * Marque l'événement comme ayant été traité
* *
* @param processingDetails Détails du traitement * @param processingDetails Détails du traitement
* @return L'instance actuelle pour chaînage * @return L'instance actuelle pour chaînage
*/ */
public AnalyticsEvent markAsProcessed(Map<String, Object> processingDetails) { public AnalyticsEvent markAsProcessed(Map<String, Object> processingDetails) {
log.debug("Marquage de l'événement {} comme traité", this.id); log.debug("Marquage de l'événement {} comme traité", this.id);
return this.addProperty("processed_at", LocalDateTime.now().toString()) return this.addProperty("processed_at", LocalDateTime.now().toString())
.addProperty("processing_details", processingDetails); .addProperty("processing_details", processingDetails);
} }
/** /**
* Suit la soumission d'un contact * Suit la soumission d'un contact
*/ */
public void trackContactSubmission() { public void trackContactSubmission() {
log.info("Suivi de la soumission pour l'événement: {}", this); log.info("Suivi de la soumission pour l'événement: {}", this);
this.addProperty("submission_tracked", true) this.addProperty("submission_tracked", true)
.addProperty("submission_time", LocalDateTime.now().toString()); .addProperty("submission_time", LocalDateTime.now().toString());
} }
@Override @Override
public String toString() { public String toString() {
return String.format( return String.format(
"AnalyticsEvent[id=%d, type=%s, userId=%s, timestamp=%s, env=%s]", "AnalyticsEvent[id=%d, type=%s, userId=%s, timestamp=%s, env=%s]",
id, eventType, userId, timestamp, environment id, eventType, userId, timestamp, environment
); );
} }
/** /**
* Crée une copie de l'événement * Crée une copie de l'événement
* *
* @return Nouvelle instance avec les mêmes données * @return Nouvelle instance avec les mêmes données
*/ */
public AnalyticsEvent copy() { public AnalyticsEvent copy() {
return this.toBuilder().build(); return this.toBuilder().build();
} }
} }

View File

@@ -1,31 +1,31 @@
package dev.lions.events; package dev.lions.events;
import dev.lions.events.AnalyticsEvent; import dev.lions.events.AnalyticsEvent;
import dev.lions.exceptions.EventPublicationException; import dev.lions.exceptions.EventPublicationException;
/** /**
* Interface définissant les opérations de publication des événements analytiques. * Interface définissant les opérations de publication des événements analytiques.
* Cette interface fournit les méthodes nécessaires pour publier des événements * Cette interface fournit les méthodes nécessaires pour publier des événements
* de manière individuelle ou par lot. * de manière individuelle ou par lot.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.0 * @version 1.0
*/ */
public interface AnalyticsEventPublisher { public interface AnalyticsEventPublisher {
/** /**
* Publie un événement analytique unique. * Publie un événement analytique unique.
* *
* @param event L'événement à publier * @param event L'événement à publier
* @throws EventPublicationException Si la publication échoue * @throws EventPublicationException Si la publication échoue
*/ */
void publish(AnalyticsEvent event) throws EventPublicationException; void publish(AnalyticsEvent event) throws EventPublicationException;
/** /**
* Publie un lot d'événements analytiques. * Publie un lot d'événements analytiques.
* *
* @param events Collection d'événements à publier * @param events Collection d'événements à publier
* @throws EventPublicationException Si la publication d'un des événements échoue * @throws EventPublicationException Si la publication d'un des événements échoue
*/ */
void publishBatch(Iterable<AnalyticsEvent> events) throws EventPublicationException; void publishBatch(Iterable<AnalyticsEvent> events) throws EventPublicationException;
} }

View File

@@ -1,43 +1,43 @@
package dev.lions.events; package dev.lions.events;
import java.util.Map; import java.util.Map;
/** /**
* Événement déclenché lors de l'initialisation du service de configuration. * Événement déclenché lors de l'initialisation du service de configuration.
* Cet événement permet de notifier les observateurs des changements de * Cet événement permet de notifier les observateurs des changements de
* configuration de l'application. * configuration de l'application.
*/ */
public class ConfigurationEvent { public class ConfigurationEvent {
private final String type; private final String type;
private final Map<String, Object> data; private final Map<String, Object> data;
/** /**
* Crée une nouvelle instance de ConfigurationEvent. * Crée une nouvelle instance de ConfigurationEvent.
* *
* @param type Type de l'événement de configuration * @param type Type de l'événement de configuration
* @param data Données associées à l'événement * @param data Données associées à l'événement
*/ */
public ConfigurationEvent(String type, Map<String, Object> data) { public ConfigurationEvent(String type, Map<String, Object> data) {
this.type = type; this.type = type;
this.data = data; this.data = data;
} }
/** /**
* Récupère le type de l'événement de configuration. * Récupère le type de l'événement de configuration.
* *
* @return Type de l'événement * @return Type de l'événement
*/ */
public String getType() { public String getType() {
return type; return type;
} }
/** /**
* Récupère les données associées à l'événement de configuration. * Récupère les données associées à l'événement de configuration.
* *
* @return Données de l'événement * @return Données de l'événement
*/ */
public Map<String, Object> getData() { public Map<String, Object> getData() {
return data; return data;
} }
} }

View File

@@ -1,60 +1,60 @@
package dev.lions.events; package dev.lions.events;
import dev.lions.exceptions.EventProcessingException; import dev.lions.exceptions.EventProcessingException;
import dev.lions.models.Contact; import dev.lions.models.Contact;
import dev.lions.services.NotificationService; import dev.lions.services.NotificationService;
import dev.lions.models.NotificationType; import dev.lions.models.NotificationType;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes; import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* Gestionnaire des événements liés aux contacts. Traite les soumissions de formulaires de contact * Gestionnaire des événements liés aux contacts. Traite les soumissions de formulaires de contact
* et déclenche les actions appropriées. * et déclenche les actions appropriées.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class ContactEventHandler { public class ContactEventHandler {
@Inject @Inject
private NotificationService notificationService; private NotificationService notificationService;
public void onContactSubmission(@Observes ContactSubmissionEvent event) { public void onContactSubmission(@Observes ContactSubmissionEvent event) {
try { try {
Contact contact = event.getContact(); Contact contact = event.getContact();
processAnalytics(contact); processAnalytics(contact);
sendNotifications(contact); sendNotifications(contact);
log.info("Contact submission event processed successfully for contact ID: {}", log.info("Contact submission event processed successfully for contact ID: {}",
contact.getId()); contact.getId());
} catch (Exception e) { } catch (Exception e) {
log.error("Error processing contact submission event", e); log.error("Error processing contact submission event", e);
throw new EventProcessingException("Failed to process contact submission", e); throw new EventProcessingException("Failed to process contact submission", e);
} }
} }
private void processAnalytics(Contact contact) { private void processAnalytics(Contact contact) {
Map<String, Object> properties = new HashMap<>(); Map<String, Object> properties = new HashMap<>();
properties.put("subject", contact.getSubject()); properties.put("subject", contact.getSubject());
properties.put("hasCompany", contact.getCompany() != null); properties.put("hasCompany", contact.getCompany() != null);
properties.put("submissionTime", contact.getSubmitDate()); properties.put("submissionTime", contact.getSubmitDate());
AnalyticsEvent analyticsEvent = AnalyticsEvent analyticsEvent =
AnalyticsEvent.builder().eventType("CONTACT_SUBMISSION").contactId( AnalyticsEvent.builder().eventType("CONTACT_SUBMISSION").contactId(
String.valueOf(contact.getId())) String.valueOf(contact.getId()))
.properties(properties).build(); .properties(properties).build();
analyticsEvent.trackContactSubmission(); analyticsEvent.trackContactSubmission();
} }
private void sendNotifications(Contact contact) { private void sendNotifications(Contact contact) {
notificationService.sendInternalNotification(NotificationType.NEW_CONTACT, notificationService.sendInternalNotification(NotificationType.NEW_CONTACT,
String.format("Nouveau message de %s : %s", String.format("Nouveau message de %s : %s",
contact.getName(), contact.getName(),
contact.getSubject())); contact.getSubject()));
} }
} }

View File

@@ -1,23 +1,23 @@
package dev.lions.events; package dev.lions.events;
import dev.lions.models.Contact; import dev.lions.models.Contact;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import lombok.Getter; import lombok.Getter;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
/** /**
* Événement émis lors de la soumission d'un nouveau formulaire de contact. * Événement émis lors de la soumission d'un nouveau formulaire de contact.
* Cet événement permet de découpler le traitement des contacts de leur soumission. * Cet événement permet de découpler le traitement des contacts de leur soumission.
*/ */
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public class ContactSubmissionEvent { public class ContactSubmissionEvent {
private final Contact contact; private final Contact contact;
private final LocalDateTime timestamp; private final LocalDateTime timestamp;
public ContactSubmissionEvent(Contact contact) { public ContactSubmissionEvent(Contact contact) {
this.contact = contact; this.contact = contact;
this.timestamp = LocalDateTime.now(); this.timestamp = LocalDateTime.now();
} }
} }

View File

@@ -1,16 +1,16 @@
package dev.lions.events; package dev.lions.events;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
/** /**
* Événement de notification lors du téléchargement d'un fichier. * Événement de notification lors du téléchargement d'un fichier.
*/ */
@Getter @Getter
@Builder @Builder
public class FileUploadEvent { public class FileUploadEvent {
private String fileId; private String fileId;
private String fileName; private String fileName;
private long size; private long size;
private long timestamp; private long timestamp;
} }

View File

@@ -1,19 +1,19 @@
package dev.lions.events; package dev.lions.events;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import java.util.Map; import java.util.Map;
/** /**
* Événement pour la navigation. * Événement pour la navigation.
*/ */
@Getter @Getter
@Builder @Builder
public class NavigationEvent { public class NavigationEvent {
private String action; private String action;
private String source; private String source;
private String destination; private String destination;
private Map<String, Object> parameters; // Nouveau champ pour les paramètres private Map<String, Object> parameters; // Nouveau champ pour les paramètres
private long timestamp; // Nouveau champ pour le timestamp private long timestamp; // Nouveau champ pour le timestamp
} }

View File

@@ -1,31 +1,31 @@
package dev.lions.events; package dev.lions.events;
import dev.lions.exceptions.EventProcessingException; import dev.lions.exceptions.EventProcessingException;
import dev.lions.utils.CacheService; import dev.lions.utils.CacheService;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes; import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.util.logging.Logger; import java.util.logging.Logger;
@ApplicationScoped @ApplicationScoped
public class ProjectEventHandler { public class ProjectEventHandler {
private static final Logger log = Logger.getLogger(ProjectEventHandler.class.getName()); private static final Logger log = Logger.getLogger(ProjectEventHandler.class.getName());
@Inject @Inject
private CacheService cacheService; private CacheService cacheService;
public void onProjectUpdate(@Observes ProjectUpdateEvent event) { public void onProjectUpdate(@Observes ProjectUpdateEvent event) {
try { try {
// Invalidation du cache // Invalidation du cache
cacheService.invalidateProjectCache(event.getProjectId()); cacheService.invalidateProjectCache(event.getProjectId());
log.info("Project event processed successfully. Action: " + event.getAction() + log.info("Project event processed successfully. Action: " + event.getAction() +
", Project ID: " + event.getProjectId()); ", Project ID: " + event.getProjectId());
} catch (Exception e) { } catch (Exception e) {
log.severe("Error processing project event: " + e.getMessage()); log.severe("Error processing project event: " + e.getMessage());
throw new EventProcessingException("Failed to process project event", e); throw new EventProcessingException("Failed to process project event", e);
} }
} }
} }

View File

@@ -1,49 +1,49 @@
package dev.lions.events; package dev.lions.events;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* Événement émis lors de la modification ou création d'un projet. Permet de gérer les mises à jour * Événement émis lors de la modification ou création d'un projet. Permet de gérer les mises à jour
* asynchrones (cache, indexation, etc.). * asynchrones (cache, indexation, etc.).
*/ */
public class ProjectUpdateEvent { public class ProjectUpdateEvent {
private final String projectId; private final String projectId;
private final String action; // CREATE, UPDATE, DELETE private final String action; // CREATE, UPDATE, DELETE
private final LocalDateTime timestamp; private final LocalDateTime timestamp;
public ProjectUpdateEvent(String projectId, String action, LocalDateTime timestamp) { public ProjectUpdateEvent(String projectId, String action, LocalDateTime timestamp) {
this.projectId = projectId; this.projectId = projectId;
this.action = action; this.action = action;
this.timestamp = timestamp; this.timestamp = timestamp;
} }
public ProjectUpdateEvent(String projectId, String action) { public ProjectUpdateEvent(String projectId, String action) {
this.projectId = projectId; this.projectId = projectId;
this.action = action; this.action = action;
this.timestamp = LocalDateTime.now(); this.timestamp = LocalDateTime.now();
} }
public String getProjectId() { public String getProjectId() {
return projectId; return projectId;
} }
public String getAction() { public String getAction() {
return action; return action;
} }
public LocalDateTime getTimestamp() { public LocalDateTime getTimestamp() {
return timestamp; return timestamp;
} }
public boolean isCreate() { public boolean isCreate() {
return "CREATE".equals(action); return "CREATE".equals(action);
} }
public boolean isUpdate() { public boolean isUpdate() {
return "UPDATE".equals(action); return "UPDATE".equals(action);
} }
public boolean isDelete() { public boolean isDelete() {
return "DELETE".equals(action); return "DELETE".equals(action);
} }
} }

View File

@@ -1,80 +1,80 @@
package dev.lions.events; package dev.lions.events;
import dev.lions.exceptions.EventPublicationException; import dev.lions.exceptions.EventPublicationException;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event; import jakarta.enterprise.event.Event;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Implémentation du publisher d'événements analytiques utilisant le système * Implémentation du publisher d'événements analytiques utilisant le système
* d'événements CDI de Quarkus pour le traitement asynchrone. * d'événements CDI de Quarkus pour le traitement asynchrone.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.1 * @version 1.1
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class QuarkusAnalyticsEventPublisher implements AnalyticsEventPublisher { public class QuarkusAnalyticsEventPublisher implements AnalyticsEventPublisher {
@Inject @Inject
Event<AnalyticsEvent> eventBus; Event<AnalyticsEvent> eventBus;
/** /**
* Publie un événement analytique de manière asynchrone. * Publie un événement analytique de manière asynchrone.
* *
* @param event L'événement à publier * @param event L'événement à publier
* @throws EventPublicationException Si la publication échoue * @throws EventPublicationException Si la publication échoue
*/ */
@Override @Override
public void publish(AnalyticsEvent event) { public void publish(AnalyticsEvent event) {
log.debug("Publication d'un événement analytique de type: {}", event.getEventType()); log.debug("Publication d'un événement analytique de type: {}", event.getEventType());
try { try {
eventBus.fireAsync(event) eventBus.fireAsync(event)
.handle((success, error) -> { .handle((success, error) -> {
if (error != null) { if (error != null) {
log.error("Erreur lors de la publication de l'événement analytique: {}", log.error("Erreur lors de la publication de l'événement analytique: {}",
error.getMessage(), error); error.getMessage(), error);
throw new EventPublicationException( throw new EventPublicationException(
"Échec de la publication de l'événement analytique", error); "Échec de la publication de l'événement analytique", error);
} else { } else {
log.debug("Événement analytique publié avec succès: {}", log.debug("Événement analytique publié avec succès: {}",
event.getEventType()); event.getEventType());
} }
return null; return null;
}); });
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur inattendue lors de la publication de l'événement", e); log.error("Erreur inattendue lors de la publication de l'événement", e);
throw new EventPublicationException( throw new EventPublicationException(
"Échec de la publication de l'événement analytique", e); "Échec de la publication de l'événement analytique", e);
} }
} }
/** /**
* Publie un lot d'événements analytiques. * Publie un lot d'événements analytiques.
* *
* @param events Collection d'événements à publier * @param events Collection d'événements à publier
* @throws EventPublicationException Si la publication d'un des événements échoue * @throws EventPublicationException Si la publication d'un des événements échoue
*/ */
@Override @Override
public void publishBatch(Iterable<AnalyticsEvent> events) { public void publishBatch(Iterable<AnalyticsEvent> events) {
log.debug("Début de la publication du lot d'événements"); log.debug("Début de la publication du lot d'événements");
AtomicInteger count = new AtomicInteger(0); AtomicInteger count = new AtomicInteger(0);
try { try {
events.forEach(event -> { events.forEach(event -> {
publish(event); publish(event);
count.incrementAndGet(); count.incrementAndGet();
}); });
log.info("Lot de {} événements publié avec succès", count.get()); log.info("Lot de {} événements publié avec succès", count.get());
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de la publication du lot après {} événements", count.get(), e); log.error("Erreur lors de la publication du lot après {} événements", count.get(), e);
throw new EventPublicationException( throw new EventPublicationException(
String.format("Échec de la publication du lot après %d événements", count.get()), String.format("Échec de la publication du lot après %d événements", count.get()),
e); e);
} }
} }
} }

View File

@@ -1,42 +1,42 @@
package dev.lions.events; package dev.lions.events;
import java.util.Map; import java.util.Map;
/** /**
* Événement déclenché lors de l'initialisation du service de stockage. Cet événement permet de * Événement déclenché lors de l'initialisation du service de stockage. Cet événement permet de
* notifier les observateurs des changements de configuration du stockage de l'application. * notifier les observateurs des changements de configuration du stockage de l'application.
*/ */
public class StorageEvent { public class StorageEvent {
private final String type; private final String type;
private final Map<String, Object> data; private final Map<String, Object> data;
/** /**
* Crée une nouvelle instance de StorageEvent. * Crée une nouvelle instance de StorageEvent.
* *
* @param type Type de l'événement de stockage * @param type Type de l'événement de stockage
* @param data Données associées à l'événement * @param data Données associées à l'événement
*/ */
public StorageEvent(String type, Map<String, Object> data) { public StorageEvent(String type, Map<String, Object> data) {
this.type = type; this.type = type;
this.data = data; this.data = data;
} }
/** /**
* Récupère le type de l'événement de stockage. * Récupère le type de l'événement de stockage.
* *
* @return Type de l'événement * @return Type de l'événement
*/ */
public String getType() { public String getType() {
return type; return type;
} }
/** /**
* Récupère les données associées à l'événement de stockage. * Récupère les données associées à l'événement de stockage.
* *
* @return Données de l'événement * @return Données de l'événement
*/ */
public Map<String, Object> getData() { public Map<String, Object> getData() {
return data; return data;
} }
} }

View File

@@ -1,17 +1,17 @@
package dev.lions.exceptions; package dev.lions.exceptions;
/** /**
* Exception spécifique pour les erreurs liées au traitement des événements analytiques. Cette * Exception spécifique pour les erreurs liées au traitement des événements analytiques. Cette
* exception encapsule les erreurs qui surviennent lors de l'enregistrement, l'enrichissement ou la * exception encapsule les erreurs qui surviennent lors de l'enregistrement, l'enrichissement ou la
* publication des événements d'analyse. * publication des événements d'analyse.
*/ */
public class AnalyticsException extends RuntimeException { public class AnalyticsException extends RuntimeException {
public AnalyticsException(String message) { public AnalyticsException(String message) {
super(message); super(message);
} }
public AnalyticsException(String message, Throwable cause) { public AnalyticsException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,12 +1,12 @@
package dev.lions.exceptions; package dev.lions.exceptions;
public class BusinessException extends RuntimeException { public class BusinessException extends RuntimeException {
public BusinessException(String message) { public BusinessException(String message) {
super(message); super(message);
} }
public BusinessException(String message, Throwable cause) { public BusinessException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,29 +1,29 @@
package dev.lions.exceptions; package dev.lions.exceptions;
/** /**
* Exception levée lorsqu'une erreur de configuration se produit. * Exception levée lorsqu'une erreur de configuration se produit.
* Cette exception encapsule les erreurs liées à la configuration * Cette exception encapsule les erreurs liées à la configuration
* de l'application, telles que des paramètres invalides ou des * de l'application, telles que des paramètres invalides ou des
* ressources indisponibles. * ressources indisponibles.
*/ */
public class ConfigurationException extends RuntimeException { public class ConfigurationException extends RuntimeException {
/** /**
* Crée une nouvelle instance de ConfigurationException avec un message. * Crée une nouvelle instance de ConfigurationException avec un message.
* *
* @param message Message décrivant l'erreur de configuration * @param message Message décrivant l'erreur de configuration
*/ */
public ConfigurationException(String message) { public ConfigurationException(String message) {
super(message); super(message);
} }
/** /**
* Crée une nouvelle instance de ConfigurationException avec un message et une cause. * Crée une nouvelle instance de ConfigurationException avec un message et une cause.
* *
* @param message Message décrivant l'erreur de configuration * @param message Message décrivant l'erreur de configuration
* @param cause Cause à l'origine de l'exception * @param cause Cause à l'origine de l'exception
*/ */
public ConfigurationException(String message, Throwable cause) { public ConfigurationException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,103 +1,103 @@
package dev.lions.exceptions; package dev.lions.exceptions;
import lombok.Getter; import lombok.Getter;
/** /**
* Exception spécifique pour la gestion des erreurs liées aux tables de données. * Exception spécifique pour la gestion des erreurs liées aux tables de données.
* Cette exception encapsule les problèmes survenant lors de la manipulation, * Cette exception encapsule les problèmes survenant lors de la manipulation,
* du tri ou du filtrage des données tabulaires. * du tri ou du filtrage des données tabulaires.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.0 * @version 1.0
*/ */
@Getter @Getter
public class DataTableException extends BusinessException { public class DataTableException extends BusinessException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Identifiant de la table concernée par l'erreur * Identifiant de la table concernée par l'erreur
* -- GETTER -- * -- GETTER --
* Récupère l'identifiant de la table concernée. * Récupère l'identifiant de la table concernée.
* *
* @return Identifiant de la table ou null si non spécifié * @return Identifiant de la table ou null si non spécifié
*/ */
private final String tableId; private final String tableId;
/** /**
* Type d'opération ayant échoué * Type d'opération ayant échoué
* -- GETTER -- * -- GETTER --
* Récupère l'opération ayant échoué. * Récupère l'opération ayant échoué.
* *
* @return Type d'opération ou null si non spécifié * @return Type d'opération ou null si non spécifié
*/ */
private final DataTableOperation operation; private final DataTableOperation operation;
/** /**
* Crée une nouvelle instance avec un message d'erreur. * Crée une nouvelle instance avec un message d'erreur.
* *
* @param message Description détaillée de l'erreur * @param message Description détaillée de l'erreur
*/ */
public DataTableException(String message) { public DataTableException(String message) {
this(message, null, null, null); this(message, null, null, null);
} }
/** /**
* Crée une nouvelle instance avec un message et une cause. * Crée une nouvelle instance avec un message et une cause.
* *
* @param message Description détaillée de l'erreur * @param message Description détaillée de l'erreur
* @param cause Cause originale de l'erreur * @param cause Cause originale de l'erreur
*/ */
public DataTableException(String message, Throwable cause) { public DataTableException(String message, Throwable cause) {
this(message, cause, null, null); this(message, cause, null, null);
} }
/** /**
* Crée une nouvelle instance avec tous les détails de l'erreur. * Crée une nouvelle instance avec tous les détails de l'erreur.
* *
* @param message Description détaillée de l'erreur * @param message Description détaillée de l'erreur
* @param cause Cause originale de l'erreur * @param cause Cause originale de l'erreur
* @param tableId Identifiant de la table concernée * @param tableId Identifiant de la table concernée
* @param operation Opération ayant échoué * @param operation Opération ayant échoué
*/ */
public DataTableException(String message, Throwable cause, String tableId, DataTableOperation operation) { public DataTableException(String message, Throwable cause, String tableId, DataTableOperation operation) {
super(message, cause); super(message, cause);
this.tableId = tableId; this.tableId = tableId;
this.operation = operation; this.operation = operation;
} }
/** /**
* Types d'opérations pouvant échouer sur une table de données. * Types d'opérations pouvant échouer sur une table de données.
*/ */
@Getter @Getter
public enum DataTableOperation { public enum DataTableOperation {
SORT("Tri"), SORT("Tri"),
FILTER("Filtrage"), FILTER("Filtrage"),
PAGINATION("Pagination"), PAGINATION("Pagination"),
UPDATE("Mise à jour"), UPDATE("Mise à jour"),
LOAD("Chargement"); LOAD("Chargement");
private final String label; private final String label;
DataTableOperation(String label) { DataTableOperation(String label) {
this.label = label; this.label = label;
} }
} }
@Override @Override
public String getMessage() { public String getMessage() {
StringBuilder message = new StringBuilder(super.getMessage()); StringBuilder message = new StringBuilder(super.getMessage());
if (tableId != null) { if (tableId != null) {
message.append(" [Table: ").append(tableId).append("]"); message.append(" [Table: ").append(tableId).append("]");
} }
if (operation != null) { if (operation != null) {
message.append(" [Opération: ").append(operation.getLabel()).append("]"); message.append(" [Opération: ").append(operation.getLabel()).append("]");
} }
return message.toString(); return message.toString();
} }
} }

View File

@@ -1,7 +1,7 @@
package dev.lions.exceptions; package dev.lions.exceptions;
public class EmailException extends RuntimeException { public class EmailException extends RuntimeException {
public EmailException(String message) { public EmailException(String message) {
super(message); super(message);
} }
} }

View File

@@ -1,21 +1,21 @@
package dev.lions.exceptions; package dev.lions.exceptions;
/** /**
* Exception levée lors d'erreurs de traitement des événements. * Exception levée lors d'erreurs de traitement des événements.
* Permet de gérer de manière cohérente les erreurs dans le système événementiel. * Permet de gérer de manière cohérente les erreurs dans le système événementiel.
*/ */
public class EventProcessingException extends RuntimeException { public class EventProcessingException extends RuntimeException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public EventProcessingException(String message) { public EventProcessingException(String message) {
super(message); super(message);
} }
public EventProcessingException(String message, Throwable cause) { public EventProcessingException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
public EventProcessingException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { public EventProcessingException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace); super(message, cause, enableSuppression, writableStackTrace);
} }
} }

View File

@@ -1,15 +1,15 @@
package dev.lions.exceptions; package dev.lions.exceptions;
/** /**
* Exception levée lors d'erreurs de publication d'événements analytiques. * Exception levée lors d'erreurs de publication d'événements analytiques.
*/ */
public class EventPublicationException extends RuntimeException { public class EventPublicationException extends RuntimeException {
public EventPublicationException(String message) { public EventPublicationException(String message) {
super(message); super(message);
} }
public EventPublicationException(String message, Throwable cause) { public EventPublicationException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,135 +1,135 @@
package dev.lions.exceptions; package dev.lions.exceptions;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* Exception spécialisée pour la gestion des erreurs lors du téléchargement de fichiers. * Exception spécialisée pour la gestion des erreurs lors du téléchargement de fichiers.
* Cette classe encapsule les différents types d'erreurs pouvant survenir pendant * Cette classe encapsule les différents types d'erreurs pouvant survenir pendant
* le processus de téléchargement et de traitement des fichiers. * le processus de téléchargement et de traitement des fichiers.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.0 * @version 1.0
*/ */
@Slf4j @Slf4j
public class FileUploadException extends BusinessException { public class FileUploadException extends BusinessException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Détails techniques de l'erreur de téléchargement * Détails techniques de l'erreur de téléchargement
*/ */
private final FileUploadErrorDetails errorDetails; private final FileUploadErrorDetails errorDetails;
/** /**
* Crée une nouvelle instance avec un message d'erreur simple. * Crée une nouvelle instance avec un message d'erreur simple.
* *
* @param message Description de l'erreur * @param message Description de l'erreur
*/ */
public FileUploadException(String message) { public FileUploadException(String message) {
this(message, null, null); this(message, null, null);
log.error("Erreur de téléchargement : {}", message); log.error("Erreur de téléchargement : {}", message);
} }
/** /**
* Crée une nouvelle instance avec un message et une cause. * Crée une nouvelle instance avec un message et une cause.
* *
* @param message Description de l'erreur * @param message Description de l'erreur
* @param cause Exception à l'origine de l'erreur * @param cause Exception à l'origine de l'erreur
*/ */
public FileUploadException(String message, Throwable cause) { public FileUploadException(String message, Throwable cause) {
this(message, cause, null); this(message, cause, null);
log.error("Erreur de téléchargement : {}", message, cause); log.error("Erreur de téléchargement : {}", message, cause);
} }
/** /**
* Crée une nouvelle instance avec tous les détails de l'erreur. * Crée une nouvelle instance avec tous les détails de l'erreur.
* *
* @param message Description de l'erreur * @param message Description de l'erreur
* @param cause Exception à l'origine de l'erreur * @param cause Exception à l'origine de l'erreur
* @param errorDetails Détails techniques de l'erreur * @param errorDetails Détails techniques de l'erreur
*/ */
public FileUploadException(String message, Throwable cause, FileUploadErrorDetails errorDetails) { public FileUploadException(String message, Throwable cause, FileUploadErrorDetails errorDetails) {
super(message, cause); super(message, cause);
this.errorDetails = errorDetails; this.errorDetails = errorDetails;
log.error("Erreur de téléchargement détaillée : {} - Détails : {}", message, errorDetails); log.error("Erreur de téléchargement détaillée : {} - Détails : {}", message, errorDetails);
} }
/** /**
* Récupère les détails techniques de l'erreur. * Récupère les détails techniques de l'erreur.
* *
* @return Détails de l'erreur ou null si non disponibles * @return Détails de l'erreur ou null si non disponibles
*/ */
public FileUploadErrorDetails getErrorDetails() { public FileUploadErrorDetails getErrorDetails() {
return errorDetails; return errorDetails;
} }
/** /**
* Classe interne représentant les détails techniques d'une erreur de téléchargement. * Classe interne représentant les détails techniques d'une erreur de téléchargement.
*/ */
@Getter @Getter
@Builder @Builder
public static class FileUploadErrorDetails { public static class FileUploadErrorDetails {
private final String fileName; private final String fileName;
private final long fileSize; private final long fileSize;
private final String mimeType; private final String mimeType;
private final String uploadLocation; private final String uploadLocation;
private final String validationError; private final String validationError;
private final String processingPhase; private final String processingPhase;
@Override @Override
public String toString() { public String toString() {
return String.format( return String.format(
"FileUploadErrorDetails[fileName=%s, fileSize=%d, mimeType=%s, " + "FileUploadErrorDetails[fileName=%s, fileSize=%d, mimeType=%s, " +
"location=%s, error=%s, phase=%s]", "location=%s, error=%s, phase=%s]",
fileName, fileSize, mimeType, uploadLocation, validationError, processingPhase fileName, fileSize, mimeType, uploadLocation, validationError, processingPhase
); );
} }
} }
/** /**
* Crée une instance d'exception pour un fichier trop volumineux. * Crée une instance d'exception pour un fichier trop volumineux.
* *
* @param fileName Nom du fichier * @param fileName Nom du fichier
* @param actualSize Taille réelle du fichier * @param actualSize Taille réelle du fichier
* @param maxSize Taille maximale autorisée * @param maxSize Taille maximale autorisée
* @return Instance de FileUploadException * @return Instance de FileUploadException
*/ */
public static FileUploadException fileTooLarge(String fileName, long actualSize, long maxSize) { public static FileUploadException fileTooLarge(String fileName, long actualSize, long maxSize) {
String message = String.format( String message = String.format(
"Le fichier '%s' est trop volumineux (%d octets). Maximum autorisé : %d octets", "Le fichier '%s' est trop volumineux (%d octets). Maximum autorisé : %d octets",
fileName, actualSize, maxSize fileName, actualSize, maxSize
); );
FileUploadErrorDetails details = FileUploadErrorDetails.builder() FileUploadErrorDetails details = FileUploadErrorDetails.builder()
.fileName(fileName) .fileName(fileName)
.fileSize(actualSize) .fileSize(actualSize)
.validationError("FILE_TOO_LARGE") .validationError("FILE_TOO_LARGE")
.processingPhase("VALIDATION") .processingPhase("VALIDATION")
.build(); .build();
return new FileUploadException(message, null, details); return new FileUploadException(message, null, details);
} }
/** /**
* Crée une instance d'exception pour un type de fichier non autorisé. * Crée une instance d'exception pour un type de fichier non autorisé.
* *
* @param fileName Nom du fichier * @param fileName Nom du fichier
* @param mimeType Type MIME du fichier * @param mimeType Type MIME du fichier
* @return Instance de FileUploadException * @return Instance de FileUploadException
*/ */
public static FileUploadException invalidFileType(String fileName, String mimeType) { public static FileUploadException invalidFileType(String fileName, String mimeType) {
String message = String.format( String message = String.format(
"Le type de fichier '%s' n'est pas autorisé pour '%s'", "Le type de fichier '%s' n'est pas autorisé pour '%s'",
mimeType, fileName mimeType, fileName
); );
FileUploadErrorDetails details = FileUploadErrorDetails details =
FileUploadErrorDetails.builder().fileName(fileName).mimeType(mimeType) FileUploadErrorDetails.builder().fileName(fileName).mimeType(mimeType)
.validationError("INVALID_FILE_TYPE").processingPhase("VALIDATION") .validationError("INVALID_FILE_TYPE").processingPhase("VALIDATION")
.fileSize(-1).build(); .fileSize(-1).build();
return new FileUploadException(message, null, details); return new FileUploadException(message, null, details);
} }
} }

View File

@@ -1,158 +1,158 @@
package dev.lions.exceptions; package dev.lions.exceptions;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* Exception spécialisée pour la gestion des erreurs de filtrage. * Exception spécialisée pour la gestion des erreurs de filtrage.
* Cette classe encapsule les différentes erreurs pouvant survenir lors * Cette classe encapsule les différentes erreurs pouvant survenir lors
* de l'application ou la manipulation des filtres de données. * de l'application ou la manipulation des filtres de données.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.0 * @version 1.0
*/ */
@Slf4j @Slf4j
public class FilterException extends BusinessException { public class FilterException extends BusinessException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Contexte détaillé de l'erreur de filtrage * Contexte détaillé de l'erreur de filtrage
*/ */
private final FilterContext filterContext; private final FilterContext filterContext;
/** /**
* Crée une nouvelle instance avec un message d'erreur simple. * Crée une nouvelle instance avec un message d'erreur simple.
* *
* @param message Description de l'erreur * @param message Description de l'erreur
*/ */
public FilterException(String message) { public FilterException(String message) {
this(message, null, null); this(message, null, null);
log.error("Erreur de filtrage : {}", message); log.error("Erreur de filtrage : {}", message);
} }
/** /**
* Crée une nouvelle instance avec un message et une cause. * Crée une nouvelle instance avec un message et une cause.
* *
* @param message Description de l'erreur * @param message Description de l'erreur
* @param cause Exception à l'origine de l'erreur * @param cause Exception à l'origine de l'erreur
*/ */
public FilterException(String message, Throwable cause) { public FilterException(String message, Throwable cause) {
this(message, cause, null); this(message, cause, null);
log.error("Erreur de filtrage : {}", message, cause); log.error("Erreur de filtrage : {}", message, cause);
} }
/** /**
* Crée une nouvelle instance avec tous les détails de l'erreur. * Crée une nouvelle instance avec tous les détails de l'erreur.
* *
* @param message Description de l'erreur * @param message Description de l'erreur
* @param cause Exception à l'origine de l'erreur * @param cause Exception à l'origine de l'erreur
* @param context Contexte du filtrage au moment de l'erreur * @param context Contexte du filtrage au moment de l'erreur
*/ */
public FilterException(String message, Throwable cause, FilterContext context) { public FilterException(String message, Throwable cause, FilterContext context) {
super(message, cause); super(message, cause);
this.filterContext = context; this.filterContext = context;
log.error("Erreur de filtrage détaillée : {} - Contexte : {}", message, context); log.error("Erreur de filtrage détaillée : {} - Contexte : {}", message, context);
} }
/** /**
* Récupère le contexte de l'erreur de filtrage. * Récupère le contexte de l'erreur de filtrage.
* *
* @return Contexte de l'erreur ou null si non disponible * @return Contexte de l'erreur ou null si non disponible
*/ */
public FilterContext getFilterContext() { public FilterContext getFilterContext() {
return filterContext; return filterContext;
} }
/** /**
* Classe interne représentant le contexte d'une erreur de filtrage. * Classe interne représentant le contexte d'une erreur de filtrage.
*/ */
@Getter @Getter
@Builder @Builder
public static class FilterContext { public static class FilterContext {
private final String field; private final String field;
private final String operator; private final String operator;
private final String value; private final String value;
private final String expectedType; private final String expectedType;
private final String actualType; private final String actualType;
private final String validationError; private final String validationError;
@Override @Override
public String toString() { public String toString() {
return String.format( return String.format(
"FilterContext[field=%s, operator=%s, value=%s, expectedType=%s, actualType=%s, error=%s]", "FilterContext[field=%s, operator=%s, value=%s, expectedType=%s, actualType=%s, error=%s]",
field, operator, value, expectedType, actualType, validationError field, operator, value, expectedType, actualType, validationError
); );
} }
} }
/** /**
* Crée une exception pour un champ de filtrage invalide. * Crée une exception pour un champ de filtrage invalide.
* *
* @param fieldName Nom du champ * @param fieldName Nom du champ
* @param value Valeur invalide * @param value Valeur invalide
* @param expectedType Type attendu * @param expectedType Type attendu
* @return Instance de FilterException * @return Instance de FilterException
*/ */
public static FilterException invalidFieldValue(String fieldName, String value, String expectedType) { public static FilterException invalidFieldValue(String fieldName, String value, String expectedType) {
String message = String.format( String message = String.format(
"Valeur invalide '%s' pour le champ '%s'. Type attendu : %s", "Valeur invalide '%s' pour le champ '%s'. Type attendu : %s",
value, fieldName, expectedType value, fieldName, expectedType
); );
FilterContext context = FilterContext.builder() FilterContext context = FilterContext.builder()
.field(fieldName) .field(fieldName)
.value(value) .value(value)
.expectedType(expectedType) .expectedType(expectedType)
.validationError("INVALID_FIELD_VALUE") .validationError("INVALID_FIELD_VALUE")
.build(); .build();
return new FilterException(message, null, context); return new FilterException(message, null, context);
} }
/** /**
* Crée une exception pour un opérateur de filtre incompatible. * Crée une exception pour un opérateur de filtre incompatible.
* *
* @param operator Opérateur utilisé * @param operator Opérateur utilisé
* @param fieldName Nom du champ * @param fieldName Nom du champ
* @param fieldType Type du champ * @param fieldType Type du champ
* @return Instance de FilterException * @return Instance de FilterException
*/ */
public static FilterException incompatibleOperator(String operator, String fieldName, String fieldType) { public static FilterException incompatibleOperator(String operator, String fieldName, String fieldType) {
String message = String.format( String message = String.format(
"L'opérateur '%s' n'est pas compatible avec le champ '%s' de type %s", "L'opérateur '%s' n'est pas compatible avec le champ '%s' de type %s",
operator, fieldName, fieldType operator, fieldName, fieldType
); );
FilterContext context = FilterContext.builder() FilterContext context = FilterContext.builder()
.field(fieldName) .field(fieldName)
.operator(operator) .operator(operator)
.expectedType(fieldType) .expectedType(fieldType)
.validationError("INCOMPATIBLE_OPERATOR") .validationError("INCOMPATIBLE_OPERATOR")
.build(); .build();
return new FilterException(message, null, context); return new FilterException(message, null, context);
} }
/** /**
* Crée une exception pour une expression de filtre invalide. * Crée une exception pour une expression de filtre invalide.
* *
* @param expression Expression de filtre * @param expression Expression de filtre
* @param reason Raison de l'invalidité * @param reason Raison de l'invalidité
* @return Instance de FilterException * @return Instance de FilterException
*/ */
public static FilterException invalidFilterExpression(String expression, String reason) { public static FilterException invalidFilterExpression(String expression, String reason) {
String message = String.format( String message = String.format(
"Expression de filtre invalide '%s' : %s", "Expression de filtre invalide '%s' : %s",
expression, reason expression, reason
); );
FilterContext context = FilterContext.builder() FilterContext context = FilterContext.builder()
.value(expression) .value(expression)
.validationError("INVALID_FILTER_EXPRESSION") .validationError("INVALID_FILTER_EXPRESSION")
.build(); .build();
return new FilterException(message, null, context); return new FilterException(message, null, context);
} }
} }

View File

@@ -1,11 +1,11 @@
package dev.lions.exceptions; package dev.lions.exceptions;
public class ImageProcessingException extends BusinessException { public class ImageProcessingException extends BusinessException {
public ImageProcessingException(String message) { public ImageProcessingException(String message) {
super(message); super(message);
} }
public ImageProcessingException(String message, Throwable cause) { public ImageProcessingException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,210 +1,210 @@
package dev.lions.exceptions; package dev.lions.exceptions;
import java.util.Map; import java.util.Map;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
* Exception spécialisée pour la gestion des erreurs d'initialisation. * Exception spécialisée pour la gestion des erreurs d'initialisation.
* Cette classe traite les erreurs survenant lors de l'initialisation * Cette classe traite les erreurs survenant lors de l'initialisation
* des composants, services et ressources de l'application. * des composants, services et ressources de l'application.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.0 * @version 1.0
*/ */
@Slf4j @Slf4j
public class InitializationException extends BusinessException { public class InitializationException extends BusinessException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Contexte détaillé de l'erreur d'initialisation * Contexte détaillé de l'erreur d'initialisation
*/ */
private final InitializationContext context; private final InitializationContext context;
/** /**
* Phase d'initialisation durant laquelle l'erreur est survenue * Phase d'initialisation durant laquelle l'erreur est survenue
*/ */
private final InitializationPhase phase; private final InitializationPhase phase;
/** /**
* Crée une nouvelle instance avec un message d'erreur simple. * Crée une nouvelle instance avec un message d'erreur simple.
* *
* @param message Description de l'erreur * @param message Description de l'erreur
*/ */
public InitializationException(String message) { public InitializationException(String message) {
this(message, null, null, null); this(message, null, null, null);
log.error("Erreur d'initialisation : {}", message); log.error("Erreur d'initialisation : {}", message);
} }
/** /**
* Crée une nouvelle instance avec un message et une cause. * Crée une nouvelle instance avec un message et une cause.
* *
* @param message Description de l'erreur * @param message Description de l'erreur
* @param cause Exception à l'origine de l'erreur * @param cause Exception à l'origine de l'erreur
*/ */
public InitializationException(String message, Throwable cause) { public InitializationException(String message, Throwable cause) {
this(message, cause, null, null); this(message, cause, null, null);
log.error("Erreur d'initialisation : {}", message, cause); log.error("Erreur d'initialisation : {}", message, cause);
} }
/** /**
* Crée une nouvelle instance avec tous les détails de l'erreur. * Crée une nouvelle instance avec tous les détails de l'erreur.
* *
* @param message Description de l'erreur * @param message Description de l'erreur
* @param cause Exception à l'origine de l'erreur * @param cause Exception à l'origine de l'erreur
* @param context Contexte de l'initialisation * @param context Contexte de l'initialisation
* @param phase Phase d'initialisation * @param phase Phase d'initialisation
*/ */
public InitializationException(String message, Throwable cause, public InitializationException(String message, Throwable cause,
InitializationContext context, InitializationPhase phase) { InitializationContext context, InitializationPhase phase) {
super(message, cause); super(message, cause);
this.context = context; this.context = context;
this.phase = phase; this.phase = phase;
log.error("Erreur d'initialisation détaillée : {} - Phase : {} - Contexte : {}", log.error("Erreur d'initialisation détaillée : {} - Phase : {} - Contexte : {}",
message, phase, context); message, phase, context);
} }
/** /**
* Récupère le contexte de l'erreur d'initialisation. * Récupère le contexte de l'erreur d'initialisation.
* *
* @return Contexte de l'erreur ou null si non disponible * @return Contexte de l'erreur ou null si non disponible
*/ */
public InitializationContext getContext() { public InitializationContext getContext() {
return context; return context;
} }
/** /**
* Récupère la phase d'initialisation. * Récupère la phase d'initialisation.
* *
* @return Phase d'initialisation ou null si non disponible * @return Phase d'initialisation ou null si non disponible
*/ */
public InitializationPhase getPhase() { public InitializationPhase getPhase() {
return phase; return phase;
} }
/** /**
* Représente les différentes phases d'initialisation possibles. * Représente les différentes phases d'initialisation possibles.
*/ */
public enum InitializationPhase { public enum InitializationPhase {
CONFIGURATION("Configuration"), CONFIGURATION("Configuration"),
RESOURCE_LOADING("Chargement des ressources"), RESOURCE_LOADING("Chargement des ressources"),
DATABASE("Base de données"), DATABASE("Base de données"),
DEPENDENCY_INJECTION("Injection de dépendances"), DEPENDENCY_INJECTION("Injection de dépendances"),
SECURITY("Sécurité"), SECURITY("Sécurité"),
CACHE("Cache"), CACHE("Cache"),
SERVICE_STARTUP("Démarrage des services"); SERVICE_STARTUP("Démarrage des services");
private final String description; private final String description;
InitializationPhase(String description) { InitializationPhase(String description) {
this.description = description; this.description = description;
} }
public String getDescription() { public String getDescription() {
return description; return description;
} }
} }
/** /**
* Classe interne représentant le contexte d'une erreur d'initialisation. * Classe interne représentant le contexte d'une erreur d'initialisation.
*/ */
@Getter @Getter
@Builder @Builder
public static class InitializationContext { public static class InitializationContext {
private final String componentName; private final String componentName;
private final String resourceName; private final String resourceName;
private final String configurationKey; private final String configurationKey;
private final String expectedState; private final String expectedState;
private final String actualState; private final String actualState;
private final Map<String, String> additionalInfo; private final Map<String, String> additionalInfo;
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder() StringBuilder sb = new StringBuilder()
.append("InitializationContext[") .append("InitializationContext[")
.append("component=").append(componentName) .append("component=").append(componentName)
.append(", resource=").append(resourceName) .append(", resource=").append(resourceName)
.append(", config=").append(configurationKey); .append(", config=").append(configurationKey);
if (expectedState != null) { if (expectedState != null) {
sb.append(", expected=").append(expectedState); sb.append(", expected=").append(expectedState);
} }
if (actualState != null) { if (actualState != null) {
sb.append(", actual=").append(actualState); sb.append(", actual=").append(actualState);
} }
if (additionalInfo != null && !additionalInfo.isEmpty()) { if (additionalInfo != null && !additionalInfo.isEmpty()) {
sb.append(", info=").append(additionalInfo); sb.append(", info=").append(additionalInfo);
} }
return sb.append("]").toString(); return sb.append("]").toString();
} }
} }
/** /**
* Crée une exception pour une ressource manquante. * Crée une exception pour une ressource manquante.
* *
* @param resourceName Nom de la ressource * @param resourceName Nom de la ressource
* @param componentName Nom du composant * @param componentName Nom du composant
* @return Instance de InitializationException * @return Instance de InitializationException
*/ */
public static InitializationException resourceNotFound(String resourceName, String componentName) { public static InitializationException resourceNotFound(String resourceName, String componentName) {
String message = String.format( String message = String.format(
"Ressource requise '%s' non trouvée pour le composant '%s'", "Ressource requise '%s' non trouvée pour le composant '%s'",
resourceName, componentName resourceName, componentName
); );
InitializationContext context = InitializationContext.builder() InitializationContext context = InitializationContext.builder()
.componentName(componentName) .componentName(componentName)
.resourceName(resourceName) .resourceName(resourceName)
.build(); .build();
return new InitializationException(message, null, context, InitializationPhase.RESOURCE_LOADING); return new InitializationException(message, null, context, InitializationPhase.RESOURCE_LOADING);
} }
/** /**
* Crée une exception pour une configuration invalide. * Crée une exception pour une configuration invalide.
* *
* @param key Clé de configuration * @param key Clé de configuration
* @param expectedValue Valeur attendue * @param expectedValue Valeur attendue
* @param actualValue Valeur actuelle * @param actualValue Valeur actuelle
* @return Instance de InitializationException * @return Instance de InitializationException
*/ */
public static InitializationException invalidConfiguration(String key, public static InitializationException invalidConfiguration(String key,
String expectedValue, String actualValue) { String expectedValue, String actualValue) {
String message = String.format( String message = String.format(
"Configuration invalide pour '%s'. Attendu : %s, Actuel : %s", "Configuration invalide pour '%s'. Attendu : %s, Actuel : %s",
key, expectedValue, actualValue key, expectedValue, actualValue
); );
InitializationContext context = InitializationContext.builder() InitializationContext context = InitializationContext.builder()
.configurationKey(key) .configurationKey(key)
.expectedState(expectedValue) .expectedState(expectedValue)
.actualState(actualValue) .actualState(actualValue)
.build(); .build();
return new InitializationException(message, null, context, InitializationPhase.CONFIGURATION); return new InitializationException(message, null, context, InitializationPhase.CONFIGURATION);
} }
/** /**
* Crée une exception pour un échec de démarrage de service. * Crée une exception pour un échec de démarrage de service.
* *
* @param serviceName Nom du service * @param serviceName Nom du service
* @param reason Raison de l'échec * @param reason Raison de l'échec
* @return Instance de InitializationException * @return Instance de InitializationException
*/ */
public static InitializationException serviceStartupFailure(String serviceName, String reason) { public static InitializationException serviceStartupFailure(String serviceName, String reason) {
String message = String.format( String message = String.format(
"Échec du démarrage du service '%s' : %s", "Échec du démarrage du service '%s' : %s",
serviceName, reason serviceName, reason
); );
InitializationContext context = InitializationContext.builder() InitializationContext context = InitializationContext.builder()
.componentName(serviceName) .componentName(serviceName)
.additionalInfo(Map.of("reason", reason)) .additionalInfo(Map.of("reason", reason))
.build(); .build();
return new InitializationException(message, null, context, InitializationPhase.SERVICE_STARTUP); return new InitializationException(message, null, context, InitializationPhase.SERVICE_STARTUP);
} }
} }

View File

@@ -1,11 +1,11 @@
package dev.lions.exceptions; package dev.lions.exceptions;
/** /**
* Exception spécifique pour les erreurs de conversion JSON. * Exception spécifique pour les erreurs de conversion JSON.
*/ */
public class JsonConversionException extends RuntimeException { public class JsonConversionException extends RuntimeException {
public JsonConversionException(String message, Throwable cause) { public JsonConversionException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,7 +1,7 @@
package dev.lions.exceptions; package dev.lions.exceptions;
public class NavigationException extends RuntimeException { public class NavigationException extends RuntimeException {
public NavigationException(String message) { public NavigationException(String message) {
super(message); super(message);
} }
} }

View File

@@ -1,11 +1,11 @@
package dev.lions.exceptions; package dev.lions.exceptions;
public class NotificationException extends BusinessException { public class NotificationException extends BusinessException {
public NotificationException(String message) { public NotificationException(String message) {
super(message); super(message);
} }
public NotificationException(String message, Throwable cause) { // Ajout du paramètre cause public NotificationException(String message, Throwable cause) { // Ajout du paramètre cause
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,14 +1,14 @@
package dev.lions.exceptions; package dev.lions.exceptions;
/** /**
* Exception pour les erreurs de repository. * Exception pour les erreurs de repository.
*/ */
public class RepositoryException extends RuntimeException { public class RepositoryException extends RuntimeException {
public RepositoryException(String message) { public RepositoryException(String message) {
super(message); super(message);
} }
public RepositoryException(String message, Throwable cause) { public RepositoryException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,29 +1,29 @@
package dev.lions.exceptions; package dev.lions.exceptions;
/** /**
* Exception levée lorsqu'une erreur de configuration de stockage se produit. * Exception levée lorsqu'une erreur de configuration de stockage se produit.
* Cette exception encapsule les erreurs liées à la configuration du stockage * Cette exception encapsule les erreurs liées à la configuration du stockage
* des fichiers, telles que des chemins de stockage invalides ou un espace * des fichiers, telles que des chemins de stockage invalides ou un espace
* de stockage insuffisant. * de stockage insuffisant.
*/ */
public class StorageConfigurationException extends RuntimeException { public class StorageConfigurationException extends RuntimeException {
/** /**
* Crée une nouvelle instance de StorageConfigurationException avec un message. * Crée une nouvelle instance de StorageConfigurationException avec un message.
* *
* @param message Message décrivant l'erreur de configuration du stockage * @param message Message décrivant l'erreur de configuration du stockage
*/ */
public StorageConfigurationException(String message) { public StorageConfigurationException(String message) {
super(message); super(message);
} }
/** /**
* Crée une nouvelle instance de StorageConfigurationException avec un message et une cause. * Crée une nouvelle instance de StorageConfigurationException avec un message et une cause.
* *
* @param message Message décrivant l'erreur de configuration du stockage * @param message Message décrivant l'erreur de configuration du stockage
* @param cause Cause à l'origine de l'exception * @param cause Cause à l'origine de l'exception
*/ */
public StorageConfigurationException(String message, Throwable cause) { public StorageConfigurationException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,26 +1,26 @@
package dev.lions.exceptions; package dev.lions.exceptions;
/** /**
* Exception pour les erreurs liées au traitement des templates. * Exception pour les erreurs liées au traitement des templates.
*/ */
public class TemplateException extends RuntimeException { public class TemplateException extends RuntimeException {
/** /**
* Constructeur avec un message. * Constructeur avec un message.
* *
* @param message Message de l'erreur * @param message Message de l'erreur
*/ */
public TemplateException(String message) { public TemplateException(String message) {
super(message); super(message);
} }
/** /**
* Constructeur avec un message et une cause. * Constructeur avec un message et une cause.
* *
* @param message Message de l'erreur * @param message Message de l'erreur
* @param cause Cause de l'erreur * @param cause Cause de l'erreur
*/ */
public TemplateException(String message, Throwable cause) { public TemplateException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,11 +1,11 @@
package dev.lions.exceptions; package dev.lions.exceptions;
public class TemplateProcessingException extends Exception { public class TemplateProcessingException extends Exception {
public TemplateProcessingException(String message) { public TemplateProcessingException(String message) {
super(message); super(message);
} }
public TemplateProcessingException(String message, Throwable cause) { public TemplateProcessingException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View File

@@ -1,7 +1,7 @@
package dev.lions.exceptions; package dev.lions.exceptions;
public class WebSocketException extends RuntimeException { public class WebSocketException extends RuntimeException {
public WebSocketException(String message) { public WebSocketException(String message) {
super(message); super(message);
} }
} }

View File

@@ -1,15 +1,15 @@
package dev.lions.health; package dev.lions.health;
import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness; import org.eclipse.microprofile.health.Liveness;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
@Liveness @Liveness
@ApplicationScoped @ApplicationScoped
public class ApplicationHealthCheck implements HealthCheck { public class ApplicationHealthCheck implements HealthCheck {
@Override @Override
public HealthCheckResponse call() { public HealthCheckResponse call() {
return HealthCheckResponse.up("Application health check"); return HealthCheckResponse.up("Application health check");
} }
} }

View File

@@ -1,74 +1,74 @@
package dev.lions.models; package dev.lions.models;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated; import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@Entity @Entity
@Getter @Getter
@Setter @Setter
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Table (name = "contacts") @Table (name = "contacts")
public class Contact { public class Contact {
@Id @Id
@GeneratedValue (strategy = GenerationType.IDENTITY) @GeneratedValue (strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@NotNull @NotNull
@Size (min = 2, max = 100) @Size (min = 2, max = 100)
private String name; private String name;
@NotNull @NotNull
@jakarta.validation.constraints.Email @jakarta.validation.constraints.Email
private String email; private String email;
@Size (max = 100) @Size (max = 100)
private String company; private String company;
@Size (max = 20) @Size (max = 20)
private String phone; private String phone;
@NotNull @NotNull
@Size (min = 3, max = 200) @Size (min = 3, max = 200)
private String subject; private String subject;
@NotNull @NotNull
@Column (columnDefinition = "TEXT") @Column (columnDefinition = "TEXT")
private String message; private String message;
@NotNull @NotNull
@Enumerated (EnumType.STRING) @Enumerated (EnumType.STRING)
private ContactStatus status; private ContactStatus status;
@NotNull @NotNull
private LocalDateTime submitDate; private LocalDateTime submitDate;
private LocalDateTime processDate; private LocalDateTime processDate;
@Size (max = 500) @Size (max = 500)
private String internalNotes; private String internalNotes;
public Contact(String name, String email, String subject, String message) { public Contact(String name, String email, String subject, String message) {
this.name = name; this.name = name;
this.email = email; this.email = email;
this.subject = subject; this.subject = subject;
this.message = message; this.message = message;
this.status = ContactStatus.NEW; this.status = ContactStatus.NEW;
this.submitDate = LocalDateTime.now(); this.submitDate = LocalDateTime.now();
} }
} }

View File

@@ -1,181 +1,181 @@
package dev.lions.models; package dev.lions.models;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
/** /**
* Représente un formulaire de contact. * Représente un formulaire de contact.
* Gère les demandes de contact avec validation complète des données * Gère les demandes de contact avec validation complète des données
* et traçabilité des soumissions. * et traçabilité des soumissions.
*/ */
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class ContactForm implements Serializable { public class ContactForm implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final java.util.regex.Pattern PHONE_PATTERN = java.util.regex.Pattern.compile("^\\+?[0-9\\s-]{8,20}$"); private static final java.util.regex.Pattern PHONE_PATTERN = java.util.regex.Pattern.compile("^\\+?[0-9\\s-]{8,20}$");
private static final int MAX_COMPANY_LENGTH = 100; private static final int MAX_COMPANY_LENGTH = 100;
private static final String DEFAULT_SUBJECT = "Demande d'information"; private static final String DEFAULT_SUBJECT = "Demande d'information";
@NotNull (message = "Le nom est obligatoire") @NotNull (message = "Le nom est obligatoire")
@Size (min = 2, max = 100, message = "Le nom doit contenir entre 2 et 100 caractères") @Size (min = 2, max = 100, message = "Le nom doit contenir entre 2 et 100 caractères")
@Pattern(regexp = "^[\\p{L}\\s'-]+$", message = "Le nom contient des caractères non autorisés") @Pattern(regexp = "^[\\p{L}\\s'-]+$", message = "Le nom contient des caractères non autorisés")
private String name; private String name;
@NotNull(message = "L'email est obligatoire") @NotNull(message = "L'email est obligatoire")
@Email (message = "L'email n'est pas valide") @Email (message = "L'email n'est pas valide")
@Size(max = 100, message = "L'email ne doit pas dépasser 100 caractères") @Size(max = 100, message = "L'email ne doit pas dépasser 100 caractères")
private String email; private String email;
@NotNull(message = "Le sujet est obligatoire") @NotNull(message = "Le sujet est obligatoire")
@Size(min = 3, max = 100, message = "Le sujet doit contenir entre 3 et 100 caractères") @Size(min = 3, max = 100, message = "Le sujet doit contenir entre 3 et 100 caractères")
private String subject; private String subject;
@NotNull(message = "Le message est obligatoire") @NotNull(message = "Le message est obligatoire")
@Size(min = 10, max = 1000, message = "Le message doit contenir entre 10 et 1000 caractères") @Size(min = 10, max = 1000, message = "Le message doit contenir entre 10 et 1000 caractères")
private String message; private String message;
@Size(max = MAX_COMPANY_LENGTH, message = "Le nom de l'entreprise ne doit pas dépasser 100 caractères") @Size(max = MAX_COMPANY_LENGTH, message = "Le nom de l'entreprise ne doit pas dépasser 100 caractères")
private String company; private String company;
@Pattern(regexp = "^\\+?[0-9\\s-]{8,20}$", message = "Le format du numéro de téléphone n'est pas valide") @Pattern(regexp = "^\\+?[0-9\\s-]{8,20}$", message = "Le format du numéro de téléphone n'est pas valide")
private String phone; private String phone;
@Builder.Default @Builder.Default
private LocalDateTime submitDate = LocalDateTime.now(); private LocalDateTime submitDate = LocalDateTime.now();
@Builder.Default @Builder.Default
private ContactStatus status = ContactStatus.NEW; private ContactStatus status = ContactStatus.NEW;
@Builder.Default @Builder.Default
private Map<String, String> metadata = new HashMap<>(); private Map<String, String> metadata = new HashMap<>();
private String ipAddress; private String ipAddress;
private String userAgent; private String userAgent;
private String referer; private String referer;
/** /**
* Crée une instance de base du formulaire. * Crée une instance de base du formulaire.
* *
* @param name Nom du contact * @param name Nom du contact
* @param email Email du contact * @param email Email du contact
* @param message Message du contact * @param message Message du contact
* @return Instance de ContactForm * @return Instance de ContactForm
*/ */
public static ContactForm createBasic(String name, String email, String message) { public static ContactForm createBasic(String name, String email, String message) {
return ContactForm.builder() return ContactForm.builder()
.name(name) .name(name)
.email(email) .email(email)
.subject(DEFAULT_SUBJECT) .subject(DEFAULT_SUBJECT)
.message(message) .message(message)
.build(); .build();
} }
/** /**
* Sanitize les données du formulaire. * Sanitize les données du formulaire.
* Nettoie et normalise les entrées utilisateur. * Nettoie et normalise les entrées utilisateur.
*/ */
public void sanitize() { public void sanitize() {
if (name != null) { if (name != null) {
name = name.trim(); name = name.trim();
} }
if (email != null) { if (email != null) {
email = email.trim().toLowerCase(); email = email.trim().toLowerCase();
} }
if (company != null) { if (company != null) {
company = company.trim(); company = company.trim();
} }
if (phone != null) { if (phone != null) {
phone = phone.replaceAll("[^+0-9\\s-]", ""); phone = phone.replaceAll("[^+0-9\\s-]", "");
} }
if (message != null) { if (message != null) {
message = message.trim(); message = message.trim();
} }
} }
/** /**
* Vérifie si le numéro de téléphone est valide. * Vérifie si le numéro de téléphone est valide.
* *
* @return true si le format est valide * @return true si le format est valide
*/ */
public boolean isValidPhone() { public boolean isValidPhone() {
return phone == null || PHONE_PATTERN.matcher(phone).matches(); return phone == null || PHONE_PATTERN.matcher(phone).matches();
} }
/** /**
* Ajoute une métadonnée au formulaire. * Ajoute une métadonnée au formulaire.
* *
* @param key Clé de la métadonnée * @param key Clé de la métadonnée
* @param value Valeur de la métadonnée * @param value Valeur de la métadonnée
*/ */
public void addMetadata(String key, String value) { public void addMetadata(String key, String value) {
if (key != null && value != null) { if (key != null && value != null) {
metadata.put(key, value); metadata.put(key, value);
} }
} }
/** /**
* Met à jour le statut du formulaire. * Met à jour le statut du formulaire.
* *
* @param newStatus Nouveau statut * @param newStatus Nouveau statut
* @param reason Raison du changement (optionnel) * @param reason Raison du changement (optionnel)
*/ */
public void updateStatus(ContactStatus newStatus, String reason) { public void updateStatus(ContactStatus newStatus, String reason) {
this.status = newStatus; this.status = newStatus;
if (reason != null) { if (reason != null) {
addMetadata("statusChangeReason", reason); addMetadata("statusChangeReason", reason);
addMetadata("statusChangeDate", LocalDateTime.now().toString()); addMetadata("statusChangeDate", LocalDateTime.now().toString());
} }
} }
/** /**
* Vérifie si le formulaire est complet et valide. * Vérifie si le formulaire est complet et valide.
* *
* @return true si le formulaire est valide * @return true si le formulaire est valide
*/ */
public boolean isValid() { public boolean isValid() {
return name != null && !name.trim().isEmpty() && return name != null && !name.trim().isEmpty() &&
email != null && email.contains("@") && email != null && email.contains("@") &&
message != null && message.trim().length() >= 10 && message != null && message.trim().length() >= 10 &&
isValidPhone(); isValidPhone();
} }
/** /**
* Crée une représentation du formulaire pour les logs. * Crée une représentation du formulaire pour les logs.
* *
* @return Version sécurisée pour les logs * @return Version sécurisée pour les logs
*/ */
public String toLogString() { public String toLogString() {
return String.format("ContactForm[name=%s, email=%s, subject=%s, timestamp=%s, status=%s]", return String.format("ContactForm[name=%s, email=%s, subject=%s, timestamp=%s, status=%s]",
name, name,
email.replaceAll("(?<=.{3}).(?=.*@)", "*"), email.replaceAll("(?<=.{3}).(?=.*@)", "*"),
subject, subject,
submitDate, submitDate,
status); status);
} }
/** /**
* Vérifie si le formulaire nécessite une attention urgente. * Vérifie si le formulaire nécessite une attention urgente.
* *
* @return true si urgent * @return true si urgent
*/ */
public boolean isUrgent() { public boolean isUrgent() {
return subject != null && return subject != null &&
(subject.toLowerCase().contains("urgent") || (subject.toLowerCase().contains("urgent") ||
message.toLowerCase().contains("urgent")); message.toLowerCase().contains("urgent"));
} }
} }

View File

@@ -1,31 +1,31 @@
package dev.lions.models; package dev.lions.models;
/** /**
* Énumération représentant les différents statuts possibles * Énumération représentant les différents statuts possibles
* pour un formulaire de contact. * pour un formulaire de contact.
*/ */
public enum ContactStatus { public enum ContactStatus {
NEW("Nouveau"), NEW("Nouveau"),
IN_PROGRESS("En cours de traitement"), IN_PROGRESS("En cours de traitement"),
RESPONDED("Répondu"), RESPONDED("Répondu"),
CLOSED("Clôturé"), CLOSED("Clôturé"),
SPAM("Spam"); SPAM("Spam");
private final String label; private final String label;
/** /**
* Constructeur privé pour initialiser le libellé du statut. * Constructeur privé pour initialiser le libellé du statut.
* @param label Libellé du statut * @param label Libellé du statut
*/ */
private ContactStatus(String label) { private ContactStatus(String label) {
this.label = label; this.label = label;
} }
/** /**
* Récupère le libellé du statut. * Récupère le libellé du statut.
* @return Libellé du statut * @return Libellé du statut
*/ */
public String getLabel() { public String getLabel() {
return label; return label;
} }
} }

View File

@@ -1,16 +1,16 @@
package dev.lions.models; package dev.lions.models;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
/** /**
* Classe représentant un message email. * Classe représentant un message email.
*/ */
@Data @Data
@Builder @Builder
public class EmailMessage { public class EmailMessage {
private final String from; private final String from;
private final String to; private final String to;
private final String subject; private final String subject;
private final String htmlContent; private final String htmlContent;
} }

View File

@@ -1,85 +1,85 @@
package dev.lions.models; package dev.lions.models;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.util.Map; import java.util.Map;
import java.util.Collections; import java.util.Collections;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.Setter; import lombok.Setter;
/** /**
* Représente un modèle de email à utiliser pour l'envoi de communications. * Représente un modèle de email à utiliser pour l'envoi de communications.
* Cette classe encapsule les informations nécessaires pour générer et envoyer un email. * Cette classe encapsule les informations nécessaires pour générer et envoyer un email.
*/ */
@Data @Data
@Builder @Builder
public class EmailTemplate { public class EmailTemplate {
@NotBlank(message = "L'identifiant du modèle de courriel est obligatoire") @NotBlank(message = "L'identifiant du modèle de courriel est obligatoire")
private Long id; private Long id;
@NotBlank(message = "Le nom du modèle de courriel est obligatoire") @NotBlank(message = "Le nom du modèle de courriel est obligatoire")
@Size(max = 100, message = "Le nom du modèle ne peut pas dépasser 100 caractères") @Size(max = 100, message = "Le nom du modèle ne peut pas dépasser 100 caractères")
private String templateName; private String templateName;
@NotBlank(message = "L'objet du courriel est obligatoire") @NotBlank(message = "L'objet du courriel est obligatoire")
@Size(max = 100, message = "L'objet ne peut pas dépasser 100 caractères") @Size(max = 100, message = "L'objet ne peut pas dépasser 100 caractères")
private String subject; private String subject;
@NotBlank(message = "Le destinataire du courriel est obligatoire") @NotBlank(message = "Le destinataire du courriel est obligatoire")
@Email(message = "Le destinataire doit être une adresse email valide") @Email(message = "Le destinataire doit être une adresse email valide")
private String recipient; private String recipient;
private Map<String, String> parameters; private Map<String, String> parameters;
@Builder.Default @Builder.Default
private boolean isActive = true; private boolean isActive = true;
/** /**
* -- SETTER -- * -- SETTER --
* Met à jour la version du modèle de courriel. * Met à jour la version du modèle de courriel.
* *
* @param version Nouvelle version * @param version Nouvelle version
*/ */
@Setter @Setter
@Builder.Default @Builder.Default
private long version = 0; private long version = 0;
@NotBlank(message = "Le contenu du courriel est obligatoire") @NotBlank(message = "Le contenu du courriel est obligatoire")
@Size(max = 10000, message = "Le contenu ne peut pas dépasser 10 000 caractères") @Size(max = 10000, message = "Le contenu ne peut pas dépasser 10 000 caractères")
private String content; private String content;
/** /**
* Récupère une copie immuable des paramètres. * Récupère une copie immuable des paramètres.
* *
* @return Paramètres du modèle de courriel * @return Paramètres du modèle de courriel
*/ */
public Map<String, String> getParameters() { public Map<String, String> getParameters() {
return Collections.unmodifiableMap(parameters); return Collections.unmodifiableMap(parameters);
} }
/** /**
* Met à jour l'état d'activation du modèle de courriel. * Met à jour l'état d'activation du modèle de courriel.
* *
* @param active Nouvel état d'activation * @param active Nouvel état d'activation
*/ */
public void setActive(boolean active) { public void setActive(boolean active) {
this.isActive = active; this.isActive = active;
} }
/** /**
* Vérifie si le modèle de courriel est valide et prêt à l'emploi. * Vérifie si le modèle de courriel est valide et prêt à l'emploi.
* *
* @return true si le modèle est valide * @return true si le modèle est valide
*/ */
public boolean isValid() { public boolean isValid() {
return id != null && return id != null &&
templateName != null && !templateName.isBlank() && templateName != null && !templateName.isBlank() &&
subject != null && !subject.isBlank() && subject != null && !subject.isBlank() &&
recipient != null && !recipient.isBlank() && recipient != null && !recipient.isBlank() &&
content != null && !content.isBlank(); content != null && !content.isBlank();
} }
} }

View File

@@ -1,54 +1,54 @@
package dev.lions.models; package dev.lions.models;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
/** /**
* Représente un domaine d'expertise de l'entreprise. * Représente un domaine d'expertise de l'entreprise.
* Chaque domaine d'expertise est caractérisé par un titre, une icône, * Chaque domaine d'expertise est caractérisé par un titre, une icône,
* une description et une liste de fonctionnalités associées. * une description et une liste de fonctionnalités associées.
*/ */
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class ExpertiseArea implements Serializable { public class ExpertiseArea implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Titre du domaine d'expertise, compris entre 3 et 100 caractères. * Titre du domaine d'expertise, compris entre 3 et 100 caractères.
*/ */
@NotNull @NotNull
@Size(min = 3, max = 100) @Size(min = 3, max = 100)
private String title; private String title;
/** /**
* Icône associée au domaine d'expertise, comprise entre 3 et 50 caractères. * Icône associée au domaine d'expertise, comprise entre 3 et 50 caractères.
*/ */
@NotNull @NotNull
@Size(min = 3, max = 50) @Size(min = 3, max = 50)
private String icon; private String icon;
/** /**
* Description du domaine d'expertise, comprise entre 10 et 500 caractères. * Description du domaine d'expertise, comprise entre 10 et 500 caractères.
*/ */
@NotNull @NotNull
@Size(min = 10, max = 500) @Size(min = 10, max = 500)
private String description; private String description;
/** /**
* Liste des fonctionnalités associées au domaine d'expertise. * Liste des fonctionnalités associées au domaine d'expertise.
*/ */
private List<String> features; private List<String> features;
/** /**
* Priorité du domaine d'expertise. * Priorité du domaine d'expertise.
*/ */
private int priority; private int priority;
} }

View File

@@ -1,127 +1,127 @@
package dev.lions.models; package dev.lions.models;
import dev.lions.utils.JsonConverter; import dev.lions.utils.JsonConverter;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Convert; import jakarta.persistence.Convert;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.EnumType; import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated; import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.PrePersist; import jakarta.persistence.PrePersist;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Map; import java.util.Map;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
/** /**
* Représente une notification système à destination d'un utilisateur. * Représente une notification système à destination d'un utilisateur.
* Cette classe encapsule les informations nécessaires pour générer, stocker * Cette classe encapsule les informations nécessaires pour générer, stocker
* et afficher une notification dans l'application. * et afficher une notification dans l'application.
*/ */
@Data @Data
@Entity @Entity
@Table(name = "notifications") @Table(name = "notifications")
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class Notification { public class Notification {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(nullable = false) @Column(nullable = false)
private String title; private String title;
@Column(nullable = false, length = 1000) @Column(nullable = false, length = 1000)
private String message; private String message;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(nullable = false) @Column(nullable = false)
private NotificationType type; private NotificationType type;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(nullable = false) @Column(nullable = false)
private NotificationStatus status; private NotificationStatus status;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime timestamp; private LocalDateTime timestamp;
@Column(name = "target_user_id") @Column(name = "target_user_id")
private Long targetUserId; private Long targetUserId;
@Column(name = "source_entity_type") @Column(name = "source_entity_type")
private String sourceEntityType; private String sourceEntityType;
@Column(name = "source_entity_id") @Column(name = "source_entity_id")
private Long sourceEntityId; private Long sourceEntityId;
@Column(name = "read_timestamp") @Column(name = "read_timestamp")
private LocalDateTime readTimestamp; private LocalDateTime readTimestamp;
@Column(name = "action_url") @Column(name = "action_url")
private String actionUrl; private String actionUrl;
@Column(name = "notification_data", columnDefinition = "jsonb") @Column(name = "notification_data", columnDefinition = "jsonb")
@Convert(converter = JsonConverter.class) @Convert(converter = JsonConverter.class)
private NotificationData data; private NotificationData data;
/** /**
* Initialise les valeurs par défaut de la notification. * Initialise les valeurs par défaut de la notification.
* La date de création et le statut "non lu" sont définis ici. * La date de création et le statut "non lu" sont définis ici.
*/ */
@PrePersist @PrePersist
protected void onCreate() { protected void onCreate() {
if (timestamp == null) { if (timestamp == null) {
timestamp = LocalDateTime.now(); timestamp = LocalDateTime.now();
} }
if (status == null) { if (status == null) {
status = NotificationStatus.UNREAD; status = NotificationStatus.UNREAD;
} }
} }
/** /**
* Représente les données supplémentaires associées à la notification. * Représente les données supplémentaires associées à la notification.
* Cette classe imbriquée permet de stocker des attributs et métadonnées. * Cette classe imbriquée permet de stocker des attributs et métadonnées.
*/ */
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public static class NotificationData { public static class NotificationData {
private Map<String, Object> attributes; private Map<String, Object> attributes;
private Map<String, String> metadata; private Map<String, String> metadata;
} }
/** /**
* Vérifie si la notification a été marquée comme lue. * Vérifie si la notification a été marquée comme lue.
* *
* @return true si la notification a été lue * @return true si la notification a été lue
*/ */
public boolean isRead() { public boolean isRead() {
return NotificationStatus.READ.equals(this.status); return NotificationStatus.READ.equals(this.status);
} }
/** /**
* Vérifie si la notification est de type critique. * Vérifie si la notification est de type critique.
* *
* @return true si la notification est critique * @return true si la notification est critique
*/ */
public boolean isCritical() { public boolean isCritical() {
return type != null && type.isCritical(); return type != null && type.isCritical();
} }
/** /**
* Marque la notification comme lue. * Marque la notification comme lue.
* Met à jour le statut et la date de lecture. * Met à jour le statut et la date de lecture.
*/ */
public void markAsRead() { public void markAsRead() {
this.status = NotificationStatus.READ; this.status = NotificationStatus.READ;
this.readTimestamp = LocalDateTime.now(); this.readTimestamp = LocalDateTime.now();
} }
} }

View File

@@ -1,88 +1,88 @@
package dev.lions.models; package dev.lions.models;
/** /**
* Représente les différents statuts possibles pour une notification système. * Représente les différents statuts possibles pour une notification système.
* Chaque statut est associé à une étiquette lisible et une classe CSS * Chaque statut est associé à une étiquette lisible et une classe CSS
* pour la mise en forme de l'interface utilisateur. * pour la mise en forme de l'interface utilisateur.
*/ */
public enum NotificationStatus { public enum NotificationStatus {
UNREAD("Non lu", "notification-unread"), UNREAD("Non lu", "notification-unread"),
READ("Lu", "notification-read"), READ("Lu", "notification-read"),
ARCHIVED("Archivé", "notification-archived"), ARCHIVED("Archivé", "notification-archived"),
DELETED("Supprimé", "notification-deleted"), DELETED("Supprimé", "notification-deleted"),
PENDING("En attente", "notification-pending"), PENDING("En attente", "notification-pending"),
PROCESSING("En cours de traitement", "notification-processing"), PROCESSING("En cours de traitement", "notification-processing"),
ERROR("Erreur", "notification-error"); ERROR("Erreur", "notification-error");
private final String label; private final String label;
private final String cssClass; private final String cssClass;
/** /**
* Constructeur privé pour créer une instance de NotificationStatus. * Constructeur privé pour créer une instance de NotificationStatus.
* *
* @param label Étiquette lisible du statut * @param label Étiquette lisible du statut
* @param cssClass Classe CSS pour la mise en forme * @param cssClass Classe CSS pour la mise en forme
*/ */
private NotificationStatus(String label, String cssClass) { private NotificationStatus(String label, String cssClass) {
this.label = label; this.label = label;
this.cssClass = cssClass; this.cssClass = cssClass;
} }
/** /**
* Récupère l'étiquette lisible du statut. * Récupère l'étiquette lisible du statut.
* *
* @return Étiquette du statut * @return Étiquette du statut
*/ */
public String getLabel() { public String getLabel() {
return label; return label;
} }
/** /**
* Récupère la classe CSS associée au statut. * Récupère la classe CSS associée au statut.
* Cette classe peut être utilisée pour la mise en forme de l'interface utilisateur. * Cette classe peut être utilisée pour la mise en forme de l'interface utilisateur.
* *
* @return Classe CSS du statut * @return Classe CSS du statut
*/ */
public String getCssClass() { public String getCssClass() {
return cssClass; return cssClass;
} }
/** /**
* Vérifie si le statut correspond à une notification active. * Vérifie si le statut correspond à une notification active.
* Les notifications archivées ou supprimées ne sont pas considérées comme actives. * Les notifications archivées ou supprimées ne sont pas considérées comme actives.
* *
* @return true si la notification est active * @return true si la notification est active
*/ */
public boolean isActive() { public boolean isActive() {
return this != ARCHIVED && this != DELETED; return this != ARCHIVED && this != DELETED;
} }
/** /**
* Vérifie si le statut indique que la notification nécessite une attention particulière. * Vérifie si le statut indique que la notification nécessite une attention particulière.
* Les notifications non lues ou en erreur sont considérées comme nécessitant une attention. * Les notifications non lues ou en erreur sont considérées comme nécessitant une attention.
* *
* @return true si la notification nécessite une attention * @return true si la notification nécessite une attention
*/ */
public boolean requiresAttention() { public boolean requiresAttention() {
return this == UNREAD || this == ERROR; return this == UNREAD || this == ERROR;
} }
/** /**
* Vérifie si la transition vers un nouveau statut est autorisée. * Vérifie si la transition vers un nouveau statut est autorisée.
* Les règles de transition sont définies en fonction de l'état actuel. * Les règles de transition sont définies en fonction de l'état actuel.
* *
* @param newStatus Nouveau statut à atteindre * @param newStatus Nouveau statut à atteindre
* @return true si la transition est autorisée * @return true si la transition est autorisée
*/ */
public boolean canTransitionTo(NotificationStatus newStatus) { public boolean canTransitionTo(NotificationStatus newStatus) {
if (this == DELETED) { if (this == DELETED) {
return false; return false;
} }
if (this == ARCHIVED && newStatus != DELETED) { if (this == ARCHIVED && newStatus != DELETED) {
return false; return false;
} }
return true; return true;
} }
} }

View File

@@ -1,59 +1,59 @@
package dev.lions.models; package dev.lions.models;
import lombok.Getter; import lombok.Getter;
/** /**
* Représente les différents types de notifications utilisées dans l'application. * Représente les différents types de notifications utilisées dans l'application.
* Chaque type de notification est associé à un titre, un message par défaut et * Chaque type de notification est associé à un titre, un message par défaut et
* un indicateur de criticité. * un indicateur de criticité.
*/ */
public enum NotificationType { public enum NotificationType {
NEW_CONTACT(true, "Nouveau contact", "Un nouveau contact a été reçu"), NEW_CONTACT(true, "Nouveau contact", "Un nouveau contact a été reçu"),
PROJECT_UPDATE(false, "Mise à jour projet", "Un projet a été mis à jour"), PROJECT_UPDATE(false, "Mise à jour projet", "Un projet a été mis à jour"),
TASK_ASSIGNED(true, "Tâche assignée", "Une nouvelle tâche vous a été assignée"), TASK_ASSIGNED(true, "Tâche assignée", "Une nouvelle tâche vous a été assignée"),
COMMENT_ADDED(false, "Nouveau commentaire", "Un commentaire a été ajouté"), COMMENT_ADDED(false, "Nouveau commentaire", "Un commentaire a été ajouté"),
DEADLINE_APPROACHING(true, "Échéance proche", "Une échéance approche"), DEADLINE_APPROACHING(true, "Échéance proche", "Une échéance approche"),
SYSTEM_ALERT(true, "Alerte système", "Une alerte système requiert votre attention"), SYSTEM_ALERT(true, "Alerte système", "Une alerte système requiert votre attention"),
MAINTENANCE_SCHEDULED(false, "Maintenance planifiée", "Une maintenance est planifiée"), MAINTENANCE_SCHEDULED(false, "Maintenance planifiée", "Une maintenance est planifiée"),
USER_MENTION(true, "Mention", "Vous avez été mentionné"), USER_MENTION(true, "Mention", "Vous avez été mentionné"),
SECURITY_ALERT(true, "Alerte sécurité", "Un problème de sécurité a été détecté"), SECURITY_ALERT(true, "Alerte sécurité", "Un problème de sécurité a été détecté"),
RESOURCE_LIMIT(true, "Limite ressources", "Une limite de ressources a été atteinte"); RESOURCE_LIMIT(true, "Limite ressources", "Une limite de ressources a été atteinte");
@Getter @Getter
private final String title; private final String title;
@Getter @Getter
private final String defaultMessage; private final String defaultMessage;
private final boolean isCritical; private final boolean isCritical;
/** /**
* Constructeur privé pour créer une instance de NotificationType. * Constructeur privé pour créer une instance de NotificationType.
* *
* @param isCritical Indicateur de criticité de la notification * @param isCritical Indicateur de criticité de la notification
* @param title Titre de la notification * @param title Titre de la notification
* @param defaultMessage Message par défaut de la notification * @param defaultMessage Message par défaut de la notification
*/ */
private NotificationType(boolean isCritical, String title, String defaultMessage) { private NotificationType(boolean isCritical, String title, String defaultMessage) {
this.isCritical = isCritical; this.isCritical = isCritical;
this.title = title; this.title = title;
this.defaultMessage = defaultMessage; this.defaultMessage = defaultMessage;
} }
/** /**
* Récupère le modèle de notification sous forme de chaîne de caractères. * Récupère le modèle de notification sous forme de chaîne de caractères.
* *
* @return Modèle de notification avec le titre et le message par défaut * @return Modèle de notification avec le titre et le message par défaut
*/ */
public String getNotificationTemplate() { public String getNotificationTemplate() {
return String.format("%s : %s", this.title, this.defaultMessage); return String.format("%s : %s", this.title, this.defaultMessage);
} }
/** /**
* Indique si le type de notification est critique. * Indique si le type de notification est critique.
* Les notifications critiques nécessitent généralement une attention prioritaire. * Les notifications critiques nécessitent généralement une attention prioritaire.
* *
* @return true si le type de notification est critique * @return true si le type de notification est critique
*/ */
public boolean isCritical() { public boolean isCritical() {
return isCritical; return isCritical;
} }
} }

View File

@@ -1,51 +1,51 @@
package dev.lions.models; package dev.lions.models;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.io.Serializable; import java.io.Serializable;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
/** /**
* Représente une étape du processus de réalisation. * Représente une étape du processus de réalisation.
* Chaque étape est caractérisée par un numéro, un titre et une description. * Chaque étape est caractérisée par un numéro, un titre et une description.
*/ */
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class ProcessStep implements Serializable { public class ProcessStep implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/** /**
* Numéro de l'étape du processus. * Numéro de l'étape du processus.
*/ */
@NotNull @NotNull
private int number; private int number;
/** /**
* Titre de l'étape, compris entre 3 et 50 caractères. * Titre de l'étape, compris entre 3 et 50 caractères.
*/ */
@NotNull @NotNull
@Size(min = 3, max = 50) @Size(min = 3, max = 50)
private String title; private String title;
/** /**
* Description de l'étape, comprise entre 10 et 250 caractères. * Description de l'étape, comprise entre 10 et 250 caractères.
*/ */
@NotNull @NotNull
@Size(min = 10, max = 250) @Size(min = 10, max = 250)
private String description; private String description;
/** /**
* Détails supplémentaires de l'étape. * Détails supplémentaires de l'étape.
*/ */
private String details; private String details;
/** /**
* Duree de l'étape. * Duree de l'étape.
*/ */
private String duration; private String duration;
} }

View File

@@ -1,215 +1,215 @@
package dev.lions.models; package dev.lions.models;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp; import org.hibernate.annotations.UpdateTimestamp;
/** /**
* Entité représentant un projet dans le système. * Entité représentant un projet dans le système.
* Gère les informations complètes d'un projet, incluant ses métadonnées, * Gère les informations complètes d'un projet, incluant ses métadonnées,
* technologies, témoignages et statut. * technologies, témoignages et statut.
*/ */
@Entity @Entity
@Table( @Table(
name = "projects", name = "projects",
indexes = { indexes = {
@Index(name = "idx_project_completion_date", columnList = "completionDate"), @Index(name = "idx_project_completion_date", columnList = "completionDate"),
@Index(name = "idx_project_featured", columnList = "featured") @Index(name = "idx_project_featured", columnList = "featured")
} }
) )
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class Project implements Serializable { public class Project implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Id @Id
@NotNull(message = "L'identifiant du projet est obligatoire") @NotNull(message = "L'identifiant du projet est obligatoire")
@Pattern(regexp = "^[a-zA-Z0-9-_]+$", message = "L'identifiant ne doit contenir que des lettres, chiffres, tirets et underscores") @Pattern(regexp = "^[a-zA-Z0-9-_]+$", message = "L'identifiant ne doit contenir que des lettres, chiffres, tirets et underscores")
private String id; private String id;
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
@NotNull(message = "Le titre du projet est obligatoire") @NotNull(message = "Le titre du projet est obligatoire")
@Size(min = 3, max = 100, message = "Le titre doit contenir entre 3 et 100 caractères") @Size(min = 3, max = 100, message = "Le titre doit contenir entre 3 et 100 caractères")
private String title; private String title;
@Column(nullable = false, length = 500) @Column(nullable = false, length = 500)
@NotNull(message = "La description du projet est obligatoire") @NotNull(message = "La description du projet est obligatoire")
@Size(min = 10, max = 500, message = "La description doit contenir entre 10 et 500 caractères") @Size(min = 10, max = 500, message = "La description doit contenir entre 10 et 500 caractères")
private String description; private String description;
@Column(nullable = false, length = 250) @Column(nullable = false, length = 250)
@NotNull(message = "La description courte est obligatoire") @NotNull(message = "La description courte est obligatoire")
@Size(min = 10, max = 250, message = "La description courte doit contenir entre 10 et 250 caractères") @Size(min = 10, max = 250, message = "La description courte doit contenir entre 10 et 250 caractères")
private String shortDescription; private String shortDescription;
@Column(nullable = false) @Column(nullable = false)
@NotNull(message = "L'URL de l'image est obligatoire") @NotNull(message = "L'URL de l'image est obligatoire")
@Pattern(regexp = "^[^<>\"']*$", message = "L'URL de l'image contient des caractères non autorisés") @Pattern(regexp = "^[^<>\"']*$", message = "L'URL de l'image contient des caractères non autorisés")
private String imageUrl; private String imageUrl;
@Column(length = 100) @Column(length = 100)
@Pattern(regexp = "^[^<>\"']*$", message = "Le nom du client contient des caractères non autorisés") @Pattern(regexp = "^[^<>\"']*$", message = "Le nom du client contient des caractères non autorisés")
private String clientName; private String clientName;
@Column(nullable = false) @Column(nullable = false)
@PastOrPresent(message = "La date de réalisation ne peut pas être dans le futur") @PastOrPresent(message = "La date de réalisation ne peut pas être dans le futur")
private LocalDateTime completionDate; private LocalDateTime completionDate;
@ElementCollection @ElementCollection
@CollectionTable( @CollectionTable(
name = "project_tags", name = "project_tags",
joinColumns = @JoinColumn(name = "project_id") joinColumns = @JoinColumn(name = "project_id")
) )
@Column(name = "tag", length = 50) @Column(name = "tag", length = 50)
@Builder.Default @Builder.Default
private List<@Pattern(regexp = "^[a-zA-Z0-9-_]+$") String> tags = new ArrayList<>(); private List<@Pattern(regexp = "^[a-zA-Z0-9-_]+$") String> tags = new ArrayList<>();
@ElementCollection @ElementCollection
@CollectionTable( @CollectionTable(
name = "project_technologies", name = "project_technologies",
joinColumns = @JoinColumn(name = "project_id") joinColumns = @JoinColumn(name = "project_id")
) )
@Column(name = "technology", length = 50) @Column(name = "technology", length = 50)
@Builder.Default @Builder.Default
private List<@Pattern(regexp = "^[a-zA-Z0-9-_. ]+$") String> technologies = new ArrayList<>(); private List<@Pattern(regexp = "^[a-zA-Z0-9-_. ]+$") String> technologies = new ArrayList<>();
@Column(length = 1000) @Column(length = 1000)
@Size(max = 1000, message = "La description du challenge ne doit pas dépasser 1000 caractères") @Size(max = 1000, message = "La description du challenge ne doit pas dépasser 1000 caractères")
private String challenge; private String challenge;
@Column(length = 1000) @Column(length = 1000)
@Size(max = 1000, message = "La description de la solution ne doit pas dépasser 1000 caractères") @Size(max = 1000, message = "La description de la solution ne doit pas dépasser 1000 caractères")
private String solution; private String solution;
@Column(length = 1000) @Column(length = 1000)
@Size(max = 1000, message = "La description des résultats ne doit pas dépasser 1000 caractères") @Size(max = 1000, message = "La description des résultats ne doit pas dépasser 1000 caractères")
private String results; private String results;
@ElementCollection @ElementCollection
@CollectionTable( @CollectionTable(
name = "project_testimonials", name = "project_testimonials",
joinColumns = @JoinColumn(name = "project_id") joinColumns = @JoinColumn(name = "project_id")
) )
@Column(name = "testimonial", length = 1000) @Column(name = "testimonial", length = 1000)
@Builder.Default @Builder.Default
private List<@Size(max = 1000) String> testimonials = new ArrayList<>(); private List<@Size(max = 1000) String> testimonials = new ArrayList<>();
@Builder.Default @Builder.Default
private boolean featured = false; private boolean featured = false;
@Version @Version
private Long version; private Long version;
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
@CreationTimestamp @CreationTimestamp
private LocalDateTime createdAt; private LocalDateTime createdAt;
@Column(name = "updated_at") @Column(name = "updated_at")
@UpdateTimestamp @UpdateTimestamp
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
/** /**
* Récupère les tags de manière sécurisée. * Récupère les tags de manière sécurisée.
* *
* @return Liste immuable des tags * @return Liste immuable des tags
*/ */
public List<String> getTags() { public List<String> getTags() {
return Collections.unmodifiableList(tags); return Collections.unmodifiableList(tags);
} }
/** /**
* Récupère les technologies de manière sécurisée. * Récupère les technologies de manière sécurisée.
* *
* @return Liste immuable des technologies * @return Liste immuable des technologies
*/ */
public List<String> getTechnologies() { public List<String> getTechnologies() {
return Collections.unmodifiableList(technologies); return Collections.unmodifiableList(technologies);
} }
/** /**
* Récupère les témoignages de manière sécurisée. * Récupère les témoignages de manière sécurisée.
* *
* @return Liste immuable des témoignages * @return Liste immuable des témoignages
*/ */
public List<String> getTestimonials() { public List<String> getTestimonials() {
return Collections.unmodifiableList(testimonials); return Collections.unmodifiableList(testimonials);
} }
/** /**
* Ajoute un tag au projet. * Ajoute un tag au projet.
* *
* @param tag Tag à ajouter * @param tag Tag à ajouter
* @return true si le tag a été ajouté * @return true si le tag a été ajouté
*/ */
public boolean addTag(String tag) { public boolean addTag(String tag) {
if (tag != null && !tag.isEmpty() && !tags.contains(tag)) { if (tag != null && !tag.isEmpty() && !tags.contains(tag)) {
return tags.add(tag.trim().toLowerCase()); return tags.add(tag.trim().toLowerCase());
} }
return false; return false;
} }
/** /**
* Ajoute une technologie au projet. * Ajoute une technologie au projet.
* *
* @param technology Technologie à ajouter * @param technology Technologie à ajouter
* @return true si la technologie a été ajoutée * @return true si la technologie a été ajoutée
*/ */
public boolean addTechnology(String technology) { public boolean addTechnology(String technology) {
if (technology != null && !technology.isEmpty() && !technologies.contains(technology)) { if (technology != null && !technology.isEmpty() && !technologies.contains(technology)) {
return technologies.add(technology.trim()); return technologies.add(technology.trim());
} }
return false; return false;
} }
/** /**
* Ajoute un témoignage au projet. * Ajoute un témoignage au projet.
* *
* @param testimonial Témoignage à ajouter * @param testimonial Témoignage à ajouter
* @return true si le témoignage a été ajouté * @return true si le témoignage a été ajouté
*/ */
public boolean addTestimonial(String testimonial) { public boolean addTestimonial(String testimonial) {
if (testimonial != null && !testimonial.isEmpty()) { if (testimonial != null && !testimonial.isEmpty()) {
return testimonials.add(testimonial.trim()); return testimonials.add(testimonial.trim());
} }
return false; return false;
} }
/** /**
* Récupère le premier témoignage s'il existe. * Récupère le premier témoignage s'il existe.
* *
* @return Optional contenant le premier témoignage * @return Optional contenant le premier témoignage
*/ */
public Optional<String> getFirstTestimonial() { public Optional<String> getFirstTestimonial() {
return testimonials.isEmpty() ? Optional.empty() : return testimonials.isEmpty() ? Optional.empty() :
Optional.of(testimonials.get(0)); Optional.of(testimonials.get(0));
} }
/** /**
* Vérifie si le projet est complet et prêt à être publié. * Vérifie si le projet est complet et prêt à être publié.
* *
* @return true si le projet est complet * @return true si le projet est complet
*/ */
public boolean isComplete() { public boolean isComplete() {
return id != null && !id.isEmpty() && return id != null && !id.isEmpty() &&
title != null && !title.isEmpty() && title != null && !title.isEmpty() &&
description != null && !description.isEmpty() && description != null && !description.isEmpty() &&
imageUrl != null && !imageUrl.isEmpty() && imageUrl != null && !imageUrl.isEmpty() &&
completionDate != null; completionDate != null;
} }
} }

View File

@@ -1,181 +1,181 @@
package dev.lions.models; package dev.lions.models;
import dev.lions.utils.ImageType; import dev.lions.utils.ImageType;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* Entité représentant une image associée à un projet. * Entité représentant une image associée à un projet.
* Cette classe gère les métadonnées et le stockage des images * Cette classe gère les métadonnées et le stockage des images
* avec support pour différents types et versions. * avec support pour différents types et versions.
*/ */
@Slf4j @Slf4j
@Data @Data
@Entity @Entity
@Table(name = "project_images") @Table(name = "project_images")
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class ProjectImage implements Serializable { public class ProjectImage implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final int MAX_FILE_SIZE = 10485760; // 10MB private static final int MAX_FILE_SIZE = 10485760; // 10MB
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "project_id", nullable = false) @JoinColumn(name = "project_id", nullable = false)
@NotNull(message = "Le projet associé est requis") @NotNull(message = "Le projet associé est requis")
private Project project; private Project project;
@Column(nullable = false) @Column(nullable = false)
@NotBlank(message = "Le nom du fichier est requis") @NotBlank(message = "Le nom du fichier est requis")
@Size(max = 255, message = "Le nom du fichier ne peut pas dépasser 255 caractères") @Size(max = 255, message = "Le nom du fichier ne peut pas dépasser 255 caractères")
private String fileName; private String fileName;
@ManyToOne @ManyToOne
@JoinColumn(name = "type_id", nullable = false) @JoinColumn(name = "type_id", nullable = false)
@NotNull(message = "Le type d'image est requis") @NotNull(message = "Le type d'image est requis")
private ImageType type; private ImageType type;
@Column(nullable = false) @Column(nullable = false)
@Min(value = 1, message = "La largeur doit être positive") @Min(value = 1, message = "La largeur doit être positive")
@Max(value = 10000, message = "La largeur ne peut pas dépasser 10000 pixels") @Max(value = 10000, message = "La largeur ne peut pas dépasser 10000 pixels")
private Integer width; private Integer width;
@Column(nullable = false) @Column(nullable = false)
@Min(value = 1, message = "La hauteur doit être positive") @Min(value = 1, message = "La hauteur doit être positive")
@Max(value = 10000, message = "La hauteur ne peut pas dépasser 10000 pixels") @Max(value = 10000, message = "La hauteur ne peut pas dépasser 10000 pixels")
private Integer height; private Integer height;
@Column(nullable = false) @Column(nullable = false)
@Min(value = 1, message = "La taille du fichier doit être positive") @Min(value = 1, message = "La taille du fichier doit être positive")
@Max(value = MAX_FILE_SIZE, message = "La taille du fichier ne peut pas dépasser 10MB") @Max(value = MAX_FILE_SIZE, message = "La taille du fichier ne peut pas dépasser 10MB")
private Long fileSize; private Long fileSize;
@Column(length = 500) @Column(length = 500)
@Size(max = 500, message = "Le texte alternatif ne peut pas dépasser 500 caractères") @Size(max = 500, message = "Le texte alternatif ne peut pas dépasser 500 caractères")
private String altText; private String altText;
@Column(name = "mime_type") @Column(name = "mime_type")
@NotBlank(message = "Le type MIME est requis") @NotBlank(message = "Le type MIME est requis")
@Pattern(regexp = "^image/[a-zA-Z0-9.+-]+$", @Pattern(regexp = "^image/[a-zA-Z0-9.+-]+$",
message = "Type MIME invalide") message = "Type MIME invalide")
private String mimeType; private String mimeType;
@Column(nullable = false) @Column(nullable = false)
private LocalDateTime uploadDate; private LocalDateTime uploadDate;
@Column(name = "last_modified") @Column(name = "last_modified")
private LocalDateTime lastModified; private LocalDateTime lastModified;
@Column(name = "checksum") @Column(name = "checksum")
@NotBlank(message = "Le checksum est requis") @NotBlank(message = "Le checksum est requis")
private String checksum; private String checksum;
@Version @Version
private Long version; private Long version;
/** /**
* Initialise les champs par défaut avant la persistance. * Initialise les champs par défaut avant la persistance.
*/ */
@PrePersist @PrePersist
protected void onCreate() { protected void onCreate() {
if (uploadDate == null) { if (uploadDate == null) {
uploadDate = LocalDateTime.now(); uploadDate = LocalDateTime.now();
} }
lastModified = uploadDate; lastModified = uploadDate;
} }
/** /**
* Met à jour la date de modification avant la mise à jour. * Met à jour la date de modification avant la mise à jour.
*/ */
@PreUpdate @PreUpdate
protected void onUpdate() { protected void onUpdate() {
lastModified = LocalDateTime.now(); lastModified = LocalDateTime.now();
} }
/** /**
* Calcule le ratio d'aspect de l'image. * Calcule le ratio d'aspect de l'image.
* *
* @return Ratio largeur/hauteur * @return Ratio largeur/hauteur
*/ */
public double getAspectRatio() { public double getAspectRatio() {
return width.doubleValue() / height.doubleValue(); return width.doubleValue() / height.doubleValue();
} }
/** /**
* Vérifie si l'image est en mode portrait. * Vérifie si l'image est en mode portrait.
* *
* @return true si la hauteur est supérieure à la largeur * @return true si la hauteur est supérieure à la largeur
*/ */
public boolean isPortrait() { public boolean isPortrait() {
return height > width; return height > width;
} }
/** /**
* Vérifie si l'image est en mode paysage. * Vérifie si l'image est en mode paysage.
* *
* @return true si la largeur est supérieure à la hauteur * @return true si la largeur est supérieure à la hauteur
*/ */
public boolean isLandscape() { public boolean isLandscape() {
return width > height; return width > height;
} }
/** /**
* Vérifie si l'image est carrée. * Vérifie si l'image est carrée.
* *
* @return true si la largeur est égale à la hauteur * @return true si la largeur est égale à la hauteur
*/ */
public boolean isSquare() { public boolean isSquare() {
return width.equals(height); return width.equals(height);
} }
/** /**
* Génère une URL relative pour l'image. * Génère une URL relative pour l'image.
* *
* @return URL relative de l'image * @return URL relative de l'image
*/ */
public String getRelativeUrl() { public String getRelativeUrl() {
return String.format("/images/projects/%d/%s", project.getId(), fileName); return String.format("/images/projects/%d/%s", project.getId(), fileName);
} }
/** /**
* Vérifie si la taille de l'image est valide. * Vérifie si la taille de l'image est valide.
* *
* @return true si les dimensions sont valides * @return true si les dimensions sont valides
*/ */
public boolean hasValidDimensions() { public boolean hasValidDimensions() {
return width > 0 && width <= 10000 && return width > 0 && width <= 10000 &&
height > 0 && height <= 10000; height > 0 && height <= 10000;
} }
/** /**
* Vérifie si la taille du fichier est valide. * Vérifie si la taille du fichier est valide.
* *
* @return true si la taille est valide * @return true si la taille est valide
*/ */
public boolean hasValidFileSize() { public boolean hasValidFileSize() {
return fileSize > 0 && fileSize <= MAX_FILE_SIZE; return fileSize > 0 && fileSize <= MAX_FILE_SIZE;
} }
/** /**
* Calcule la taille en mégaoctets. * Calcule la taille en mégaoctets.
* *
* @return Taille en Mo * @return Taille en Mo
*/ */
public double getSizeInMB() { public double getSizeInMB() {
return fileSize / (1024.0 * 1024.0); return fileSize / (1024.0 * 1024.0);
} }
} }

View File

@@ -1,179 +1,179 @@
package dev.lions.models; package dev.lions.models;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
/** /**
* Représente un service proposé par l'entreprise. * Représente un service proposé par l'entreprise.
* Cette classe définit les caractéristiques et fonctionnalités d'un service, * Cette classe définit les caractéristiques et fonctionnalités d'un service,
* avec validation complète des données et gestion des états. * avec validation complète des données et gestion des états.
*/ */
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class Service implements Serializable, Comparable<Service> { public class Service implements Serializable, Comparable<Service> {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@NotNull(message = "L'identifiant du service ne peut pas être nul") @NotNull(message = "L'identifiant du service ne peut pas être nul")
@Pattern (regexp = "^[a-z0-9-]+$", message = "L'identifiant doit être en minuscules, avec chiffres et tirets uniquement") @Pattern (regexp = "^[a-z0-9-]+$", message = "L'identifiant doit être en minuscules, avec chiffres et tirets uniquement")
private String id; private String id;
@NotNull(message = "Le titre du service ne peut pas être nul") @NotNull(message = "Le titre du service ne peut pas être nul")
@Size(min = 3, max = 100, message = "Le titre doit contenir entre 3 et 100 caractères") @Size(min = 3, max = 100, message = "Le titre doit contenir entre 3 et 100 caractères")
private String title; private String title;
@NotNull(message = "L'icône du service ne peut pas être nulle") @NotNull(message = "L'icône du service ne peut pas être nulle")
@Pattern(regexp = "^fa-[a-z0-9-]+$", message = "L'icône doit suivre le format Font Awesome (ex: fa-users)") @Pattern(regexp = "^fa-[a-z0-9-]+$", message = "L'icône doit suivre le format Font Awesome (ex: fa-users)")
private String icon; private String icon;
@NotNull(message = "La description du service ne peut pas être nulle") @NotNull(message = "La description du service ne peut pas être nulle")
@Size(min = 10, max = 500, message = "La description doit contenir entre 10 et 500 caractères") @Size(min = 10, max = 500, message = "La description doit contenir entre 10 et 500 caractères")
private String description; private String description;
@Size(max = 1000, message = "La description détaillée ne doit pas dépasser 1000 caractères") @Size(max = 1000, message = "La description détaillée ne doit pas dépasser 1000 caractères")
private String longDescription; private String longDescription;
@Builder.Default @Builder.Default
private List<@Size(max = 100) String> benefits = new ArrayList<>(); private List<@Size(max = 100) String> benefits = new ArrayList<>();
@Pattern(regexp = "^/[a-z0-9-/]+$", message = "L'URL doit commencer par '/' et ne contenir que des caractères valides") @Pattern(regexp = "^/[a-z0-9-/]+$", message = "L'URL doit commencer par '/' et ne contenir que des caractères valides")
private String detailsUrl; private String detailsUrl;
@Min (value = 0, message = "La priorité doit être positive") @Min (value = 0, message = "La priorité doit être positive")
@Max (value = 100, message = "La priorité ne peut pas dépasser 100") @Max (value = 100, message = "La priorité ne peut pas dépasser 100")
@Builder.Default @Builder.Default
private int priority = 50; private int priority = 50;
@Builder.Default @Builder.Default
private boolean isActive = true; private boolean isActive = true;
@Builder.Default @Builder.Default
private LocalDateTime createdAt = LocalDateTime.now(); private LocalDateTime createdAt = LocalDateTime.now();
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
@Pattern(regexp = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", message = "La couleur doit être au format hexadécimal") @Pattern(regexp = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$", message = "La couleur doit être au format hexadécimal")
@Builder.Default @Builder.Default
private String accentColor = "#2196F3"; private String accentColor = "#2196F3";
/** /**
* Crée un service de base avec les champs obligatoires. * Crée un service de base avec les champs obligatoires.
* *
* @param title Titre du service * @param title Titre du service
* @param icon Icône du service * @param icon Icône du service
* @param description Description du service * @param description Description du service
* @param benefits Liste des avantages * @param benefits Liste des avantages
* @param detailsUrl URL des détails * @param detailsUrl URL des détails
* @return Instance de Service * @return Instance de Service
*/ */
public static Service createBasicService(String title, String icon, String description, public static Service createBasicService(String title, String icon, String description,
List<String> benefits, String detailsUrl) { List<String> benefits, String detailsUrl) {
String id = title.toLowerCase() String id = title.toLowerCase()
.replaceAll("[^a-z0-9-]", "-") .replaceAll("[^a-z0-9-]", "-")
.replaceAll("-+", "-"); .replaceAll("-+", "-");
return Service.builder() return Service.builder()
.id(id) .id(id)
.title(title) .title(title)
.icon(icon) .icon(icon)
.description(description) .description(description)
.benefits(benefits != null ? new ArrayList<>(benefits) : new ArrayList<>()) .benefits(benefits != null ? new ArrayList<>(benefits) : new ArrayList<>())
.detailsUrl(detailsUrl) .detailsUrl(detailsUrl)
.build(); .build();
} }
/** /**
* Ajoute un avantage à la liste des bénéfices. * Ajoute un avantage à la liste des bénéfices.
* *
* @param benefit Avantage à ajouter * @param benefit Avantage à ajouter
* @return true si l'ajout a réussi * @return true si l'ajout a réussi
*/ */
public boolean addBenefit(String benefit) { public boolean addBenefit(String benefit) {
if (benefit != null && !benefit.isEmpty() && benefit.length() <= 100) { if (benefit != null && !benefit.isEmpty() && benefit.length() <= 100) {
return benefits.add(benefit.trim()); return benefits.add(benefit.trim());
} }
return false; return false;
} }
/** /**
* Récupère la liste immuable des avantages. * Récupère la liste immuable des avantages.
* *
* @return Liste des avantages * @return Liste des avantages
*/ */
public List<String> getBenefits() { public List<String> getBenefits() {
return Collections.unmodifiableList(benefits); return Collections.unmodifiableList(benefits);
} }
/** /**
* Met à jour la date de modification. * Met à jour la date de modification.
*/ */
public void touch() { public void touch() {
this.updatedAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now();
} }
/** /**
* Active ou désactive le service. * Active ou désactive le service.
* *
* @param active Nouvel état * @param active Nouvel état
*/ */
public void setActive(boolean active) { public void setActive(boolean active) {
this.isActive = active; this.isActive = active;
touch(); touch();
} }
/** /**
* Vérifie si le service est complet et valide. * Vérifie si le service est complet et valide.
* *
* @return true si le service est valide * @return true si le service est valide
*/ */
public boolean isValid() { public boolean isValid() {
return id != null && !id.isEmpty() && return id != null && !id.isEmpty() &&
title != null && !title.isEmpty() && title != null && !title.isEmpty() &&
icon != null && !icon.isEmpty() && icon != null && !icon.isEmpty() &&
description != null && !description.isEmpty(); description != null && !description.isEmpty();
} }
/** /**
* Implémente la comparaison pour le tri par priorité. * Implémente la comparaison pour le tri par priorité.
*/ */
@Override @Override
public int compareTo(Service other) { public int compareTo(Service other) {
return Integer.compare(this.priority, other.priority); return Integer.compare(this.priority, other.priority);
} }
/** /**
* Construit une représentation HTML sûre de l'icône. * Construit une représentation HTML sûre de l'icône.
* *
* @return Balise HTML de l'icône * @return Balise HTML de l'icône
*/ */
public String getIconHtml() { public String getIconHtml() {
return String.format("<i class=\"fas %s\" aria-hidden=\"true\"></i>", return String.format("<i class=\"fas %s\" aria-hidden=\"true\"></i>",
icon.replaceAll("[^a-zA-Z0-9-]", "")); icon.replaceAll("[^a-zA-Z0-9-]", ""));
} }
/** /**
* Génère un identifiant unique basé sur le titre. * Génère un identifiant unique basé sur le titre.
*/ */
public void generateId() { public void generateId() {
if (this.id == null || this.id.isEmpty()) { if (this.id == null || this.id.isEmpty()) {
this.id = this.title.toLowerCase() this.id = this.title.toLowerCase()
.replaceAll("[^a-z0-9-]", "-") .replaceAll("[^a-z0-9-]", "-")
.replaceAll("-+", "-"); .replaceAll("-+", "-");
} }
} }
} }

View File

@@ -1,150 +1,150 @@
package dev.lions.models; package dev.lions.models;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Objects; import java.util.Objects;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import org.hibernate.annotations.Cache; import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.HibernateProxy;
/** /**
* Représente un témoignage de client pour un projet. * Représente un témoignage de client pour un projet.
*/ */
@Entity @Entity
@Table(name = "project_testimonials") @Table(name = "project_testimonials")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Getter @Getter
@Setter @Setter
@ToString @ToString
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor
public class Testimonial implements Serializable { public class Testimonial implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Id @Id
@NotBlank(message = "L'identifiant du témoignage est obligatoire") @NotBlank(message = "L'identifiant du témoignage est obligatoire")
private String id; private String id;
@NotBlank(message = "Le nom du client est obligatoire") @NotBlank(message = "Le nom du client est obligatoire")
@Size(min = 3, max = 100) @Size(min = 3, max = 100)
private String clientName; private String clientName;
@NotBlank(message = "Le poste du client est obligatoire") @NotBlank(message = "Le poste du client est obligatoire")
@Size(min = 3, max = 100) @Size(min = 3, max = 100)
private String clientPosition; private String clientPosition;
@NotBlank(message = "Le contenu du témoignage est obligatoire") @NotBlank(message = "Le contenu du témoignage est obligatoire")
@Size(min = 10, max = 1000) @Size(min = 10, max = 1000)
private String content; private String content;
@Size(max = 255) @Size(max = 255)
private String clientImage; private String clientImage;
@ManyToOne @ManyToOne
@JoinColumn(name = "project_id", nullable = false) @JoinColumn(name = "project_id", nullable = false)
private Project project; private Project project;
@Builder.Default @Builder.Default
private int rating = 5; private int rating = 5;
@Builder.Default @Builder.Default
private boolean isFeatured = false; private boolean isFeatured = false;
@Column(name = "completion_date") @Column(name = "completion_date")
private LocalDateTime completionDate; private LocalDateTime completionDate;
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@Column(name = "updated_at") @Column(name = "updated_at")
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
/** /**
* Constructeur par défaut requis pour JPA. * Constructeur par défaut requis pour JPA.
*/ */
public Testimonial() { public Testimonial() {
this.createdAt = LocalDateTime.now(); this.createdAt = LocalDateTime.now();
} }
/** /**
* Crée une nouvelle instance de témoignage. * Crée une nouvelle instance de témoignage.
*/ */
public static Testimonial create(String clientName, String clientPosition, String content, public static Testimonial create(String clientName, String clientPosition, String content,
String clientImage, Project project, int rating, boolean isFeatured) { String clientImage, Project project, int rating, boolean isFeatured) {
Testimonial testimonial = new Testimonial(); Testimonial testimonial = new Testimonial();
testimonial.setClientName(clientName); testimonial.setClientName(clientName);
testimonial.setClientPosition(clientPosition); testimonial.setClientPosition(clientPosition);
testimonial.setContent(content); testimonial.setContent(content);
testimonial.setClientImage(clientImage); testimonial.setClientImage(clientImage);
testimonial.setProject(project); testimonial.setProject(project);
testimonial.setRating(rating); testimonial.setRating(rating);
testimonial.setFeatured(isFeatured); testimonial.setFeatured(isFeatured);
testimonial.setCreatedAt(LocalDateTime.now()); testimonial.setCreatedAt(LocalDateTime.now());
return testimonial; return testimonial;
} }
/** /**
* Builder personnalisé pour permettre d'ajouter un titre de projet. * Builder personnalisé pour permettre d'ajouter un titre de projet.
*/ */
public static class TestimonialBuilder { public static class TestimonialBuilder {
public TestimonialBuilder projectTitle(String title) { public TestimonialBuilder projectTitle(String title) {
if (this.project == null) { if (this.project == null) {
this.project = new Project(); this.project = new Project();
} }
this.project.setTitle(title); this.project.setTitle(title);
return this; return this;
} }
public TestimonialBuilder date(LocalDateTime completionDate) { public TestimonialBuilder date(LocalDateTime completionDate) {
this.completionDate = completionDate; this.completionDate = completionDate;
return this; return this;
} }
} }
@Override @Override
public final boolean equals(Object o) { public final boolean equals(Object o) {
if (this == o) { if (this == o) {
return true; return true;
} }
if (o == null) { if (o == null) {
return false; return false;
} }
Class<?> oEffectiveClass = Class<?> oEffectiveClass =
o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer() o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer()
.getPersistentClass() : o.getClass(); .getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = Class<?> thisEffectiveClass =
this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer() this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
.getPersistentClass() .getPersistentClass()
: this.getClass(); : this.getClass();
if (thisEffectiveClass != oEffectiveClass) { if (thisEffectiveClass != oEffectiveClass) {
return false; return false;
} }
Testimonial that = (Testimonial) o; Testimonial that = (Testimonial) o;
return getId() != null && Objects.equals(getId(), that.getId()); return getId() != null && Objects.equals(getId(), that.getId());
} }
@Override @Override
public final int hashCode() { public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer() return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
.getPersistentClass().hashCode() .getPersistentClass().hashCode()
: getClass().hashCode(); : getClass().hashCode();
} }
} }

View File

@@ -0,0 +1,11 @@
package dev.lions.quote;
/**
* Niveau de complexité d'un module
*/
public enum ComplexityLevel {
BASIC, // Basique (-20%)
STANDARD, // Standard (prix de base)
ADVANCED, // Avancé (+30%)
ENTERPRISE // Enterprise (+60%)
}

View File

@@ -0,0 +1,158 @@
package dev.lions.quote;
import jakarta.persistence.*;
import java.util.List;
import java.util.ArrayList;
/**
* Catalogue des modules disponibles avec tarification
*/
@Entity
@Table(name = "module_catalog")
public class ModuleCatalog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String moduleCode;
@Column(nullable = false)
private String moduleName;
@Column(length = 2000)
private String description;
@Column(length = 3000)
private String features; // Fonctionnalités principales
// Tarification par niveau
private Double basicPrice; // Prix niveau basique
private Double standardPrice; // Prix niveau standard
private Double advancedPrice; // Prix niveau avancé
private Double enterprisePrice; // Prix niveau enterprise
// Détails techniques
@Column(length = 2000)
private String technicalRequirements;
private Integer baseImplementationDays;
private Integer maxUsers;
private String supportLevel;
// Prérequis et dépendances
@ElementCollection
@CollectionTable(name = "module_prerequisites")
private List<String> prerequisites = new ArrayList<>();
@ElementCollection
@CollectionTable(name = "module_integrations")
private List<String> integrations = new ArrayList<>();
// Métadonnées
private String category; // commercial, stock, comptabilite, rh, infrastructure
private Integer displayOrder;
private Boolean active = true;
private Boolean popular = false;
// Constructeurs
public ModuleCatalog() {}
public ModuleCatalog(String moduleCode, String moduleName, String category) {
this.moduleCode = moduleCode;
this.moduleName = moduleName;
this.category = category;
}
// Méthodes métier
public Double getPriceForComplexity(ComplexityLevel complexity) {
return switch (complexity) {
case BASIC -> basicPrice;
case STANDARD -> standardPrice;
case ADVANCED -> advancedPrice;
case ENTERPRISE -> enterprisePrice;
};
}
public ComplexityLevel getRecommendedComplexity(Double auditScore, Integer employeeCount) {
// Logique de recommandation basée sur l'audit et la taille
if (auditScore < 30 || employeeCount <= 5) {
return ComplexityLevel.BASIC;
} else if (auditScore < 60 || employeeCount <= 20) {
return ComplexityLevel.STANDARD;
} else if (auditScore < 80 || employeeCount <= 50) {
return ComplexityLevel.ADVANCED;
} else {
return ComplexityLevel.ENTERPRISE;
}
}
public Integer getEstimatedImplementationDays(ComplexityLevel complexity) {
double multiplier = switch (complexity) {
case BASIC -> 0.7;
case STANDARD -> 1.0;
case ADVANCED -> 1.4;
case ENTERPRISE -> 2.0;
};
return (int) Math.ceil(baseImplementationDays * multiplier);
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getModuleCode() { return moduleCode; }
public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; }
public String getModuleName() { return moduleName; }
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getFeatures() { return features; }
public void setFeatures(String features) { this.features = features; }
public Double getBasicPrice() { return basicPrice; }
public void setBasicPrice(Double basicPrice) { this.basicPrice = basicPrice; }
public Double getStandardPrice() { return standardPrice; }
public void setStandardPrice(Double standardPrice) { this.standardPrice = standardPrice; }
public Double getAdvancedPrice() { return advancedPrice; }
public void setAdvancedPrice(Double advancedPrice) { this.advancedPrice = advancedPrice; }
public Double getEnterprisePrice() { return enterprisePrice; }
public void setEnterprisePrice(Double enterprisePrice) { this.enterprisePrice = enterprisePrice; }
public String getTechnicalRequirements() { return technicalRequirements; }
public void setTechnicalRequirements(String technicalRequirements) { this.technicalRequirements = technicalRequirements; }
public Integer getBaseImplementationDays() { return baseImplementationDays; }
public void setBaseImplementationDays(Integer baseImplementationDays) { this.baseImplementationDays = baseImplementationDays; }
public Integer getMaxUsers() { return maxUsers; }
public void setMaxUsers(Integer maxUsers) { this.maxUsers = maxUsers; }
public String getSupportLevel() { return supportLevel; }
public void setSupportLevel(String supportLevel) { this.supportLevel = supportLevel; }
public List<String> getPrerequisites() { return prerequisites; }
public void setPrerequisites(List<String> prerequisites) { this.prerequisites = prerequisites; }
public List<String> getIntegrations() { return integrations; }
public void setIntegrations(List<String> integrations) { this.integrations = integrations; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public Integer getDisplayOrder() { return displayOrder; }
public void setDisplayOrder(Integer displayOrder) { this.displayOrder = displayOrder; }
public Boolean getActive() { return active; }
public void setActive(Boolean active) { this.active = active; }
public Boolean getPopular() { return popular; }
public void setPopular(Boolean popular) { this.popular = popular; }
}

View File

@@ -0,0 +1,245 @@
package dev.lions.quote;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.ArrayList;
/**
* Devis personnalisé généré pour une PME
*/
@Entity
@Table(name = "quotes")
public class Quote {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String quoteNumber; // QUO-2024-001
// Informations client
@Column(nullable = false)
private String companyName;
@Column(nullable = false)
private String contactName;
@Column(nullable = false)
private String email;
private String phone;
private String address;
private String sector;
private Integer employeeCount;
// Référence audit
private Long auditId;
private Double auditScore;
// Modules sélectionnés
@OneToMany(mappedBy = "quote", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<QuoteModule> modules = new ArrayList<>();
// Tarification
private Double subtotalHT; // Sous-total HT
private Double discountPercentage = 0.0; // Remise %
private Double discountAmount = 0.0; // Montant remise
private Double totalHT; // Total HT après remise
private Double vatRate = 18.0; // TVA 18% Côte d'Ivoire
private Double vatAmount; // Montant TVA
private Double totalTTC; // Total TTC
// Services additionnels
private Double formationHours = 0.0;
private Double formationRate = 15000.0; // 15K FCFA/heure
private Double supportMonths = 0.0;
private Double supportRate = 25000.0; // 25K FCFA/mois
// Conditions
private Integer validityDays = 30; // Validité 30 jours
private String paymentTerms = "50% à la commande, 50% à la livraison";
private String deliveryTerms = "6-8 semaines après signature";
// Statut
@Enumerated(EnumType.STRING)
private QuoteStatus status = QuoteStatus.DRAFT;
// Dates
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime sentAt;
private LocalDateTime viewedAt;
private LocalDateTime acceptedAt;
private LocalDateTime expiredAt;
// Suivi commercial
private String salesNotes;
private String clientFeedback;
// Constructeurs
public Quote() {
this.createdAt = LocalDateTime.now();
this.expiredAt = LocalDateTime.now().plusDays(validityDays);
}
public Quote(String companyName, String contactName, String email) {
this();
this.companyName = companyName;
this.contactName = contactName;
this.email = email;
}
// Méthodes métier
public void calculateTotals() {
// Calcul sous-total modules
this.subtotalHT = modules.stream()
.mapToDouble(m -> m.getUnitPrice() * m.getQuantity())
.sum();
// Ajout formation et support
this.subtotalHT += (formationHours * formationRate);
this.subtotalHT += (supportMonths * supportRate);
// Calcul remise
this.discountAmount = subtotalHT * (discountPercentage / 100);
this.totalHT = subtotalHT - discountAmount;
// Calcul TVA
this.vatAmount = totalHT * (vatRate / 100);
this.totalTTC = totalHT + vatAmount;
}
public void generateQuoteNumber() {
if (this.quoteNumber == null) {
int year = LocalDateTime.now().getYear();
// Le numéro sera généré par le service avec séquence
this.quoteNumber = String.format("QUO-%d-XXX", year);
}
}
public boolean isExpired() {
return LocalDateTime.now().isAfter(expiredAt) && status == QuoteStatus.SENT;
}
public boolean isValid() {
return LocalDateTime.now().isBefore(expiredAt) && status == QuoteStatus.SENT;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getQuoteNumber() { return quoteNumber; }
public void setQuoteNumber(String quoteNumber) { this.quoteNumber = quoteNumber; }
public String getCompanyName() { return companyName; }
public void setCompanyName(String companyName) { this.companyName = companyName; }
public String getContactName() { return contactName; }
public void setContactName(String contactName) { this.contactName = contactName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getSector() { return sector; }
public void setSector(String sector) { this.sector = sector; }
public Integer getEmployeeCount() { return employeeCount; }
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
public Long getAuditId() { return auditId; }
public void setAuditId(Long auditId) { this.auditId = auditId; }
public Double getAuditScore() { return auditScore; }
public void setAuditScore(Double auditScore) { this.auditScore = auditScore; }
public List<QuoteModule> getModules() { return modules; }
public void setModules(List<QuoteModule> modules) { this.modules = modules; }
public Double getSubtotalHT() { return subtotalHT; }
public void setSubtotalHT(Double subtotalHT) { this.subtotalHT = subtotalHT; }
public Double getDiscountPercentage() { return discountPercentage; }
public void setDiscountPercentage(Double discountPercentage) { this.discountPercentage = discountPercentage; }
public Double getDiscountAmount() { return discountAmount; }
public void setDiscountAmount(Double discountAmount) { this.discountAmount = discountAmount; }
public Double getTotalHT() { return totalHT; }
public void setTotalHT(Double totalHT) { this.totalHT = totalHT; }
public Double getVatRate() { return vatRate; }
public void setVatRate(Double vatRate) { this.vatRate = vatRate; }
public Double getVatAmount() { return vatAmount; }
public void setVatAmount(Double vatAmount) { this.vatAmount = vatAmount; }
public Double getTotalTTC() { return totalTTC; }
public void setTotalTTC(Double totalTTC) { this.totalTTC = totalTTC; }
public Double getFormationHours() { return formationHours; }
public void setFormationHours(Double formationHours) { this.formationHours = formationHours; }
public Double getFormationRate() { return formationRate; }
public void setFormationRate(Double formationRate) { this.formationRate = formationRate; }
public Double getSupportMonths() { return supportMonths; }
public void setSupportMonths(Double supportMonths) { this.supportMonths = supportMonths; }
public Double getSupportRate() { return supportRate; }
public void setSupportRate(Double supportRate) { this.supportRate = supportRate; }
public Integer getValidityDays() { return validityDays; }
public void setValidityDays(Integer validityDays) { this.validityDays = validityDays; }
public String getPaymentTerms() { return paymentTerms; }
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
public String getDeliveryTerms() { return deliveryTerms; }
public void setDeliveryTerms(String deliveryTerms) { this.deliveryTerms = deliveryTerms; }
public QuoteStatus getStatus() { return status; }
public void setStatus(QuoteStatus status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getSentAt() { return sentAt; }
public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; }
public LocalDateTime getViewedAt() { return viewedAt; }
public void setViewedAt(LocalDateTime viewedAt) { this.viewedAt = viewedAt; }
public LocalDateTime getAcceptedAt() { return acceptedAt; }
public void setAcceptedAt(LocalDateTime acceptedAt) { this.acceptedAt = acceptedAt; }
public LocalDateTime getExpiredAt() { return expiredAt; }
public void setExpiredAt(LocalDateTime expiredAt) { this.expiredAt = expiredAt; }
public String getSalesNotes() { return salesNotes; }
public void setSalesNotes(String salesNotes) { this.salesNotes = salesNotes; }
public String getClientFeedback() { return clientFeedback; }
public void setClientFeedback(String clientFeedback) { this.clientFeedback = clientFeedback; }
}
/**
* Statut du devis
*/
enum QuoteStatus {
DRAFT, // Brouillon
SENT, // Envoyé
VIEWED, // Consulté par le client
ACCEPTED, // Accepté
REJECTED, // Refusé
EXPIRED // Expiré
}

View File

@@ -0,0 +1,42 @@
package dev.lions.quote;
import java.util.List;
/**
* DTO pour la personnalisation de devis
*/
public class QuoteCustomizationDTO {
private List<QuoteModuleDTO> modules;
private Double formationHours;
private Double supportMonths;
private String paymentTerms;
private String deliveryTerms;
private Double discountPercentage;
private String discountReason;
// Constructeurs
public QuoteCustomizationDTO() {}
// Getters et Setters
public List<QuoteModuleDTO> getModules() { return modules; }
public void setModules(List<QuoteModuleDTO> modules) { this.modules = modules; }
public Double getFormationHours() { return formationHours; }
public void setFormationHours(Double formationHours) { this.formationHours = formationHours; }
public Double getSupportMonths() { return supportMonths; }
public void setSupportMonths(Double supportMonths) { this.supportMonths = supportMonths; }
public String getPaymentTerms() { return paymentTerms; }
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
public String getDeliveryTerms() { return deliveryTerms; }
public void setDeliveryTerms(String deliveryTerms) { this.deliveryTerms = deliveryTerms; }
public Double getDiscountPercentage() { return discountPercentage; }
public void setDiscountPercentage(Double discountPercentage) { this.discountPercentage = discountPercentage; }
public String getDiscountReason() { return discountReason; }
public void setDiscountReason(String discountReason) { this.discountReason = discountReason; }
}

View File

@@ -0,0 +1,181 @@
package dev.lions.quote;
import java.time.LocalDateTime;
import java.util.List;
import java.util.ArrayList;
/**
* DTOs pour les devis
*/
public class QuoteDTO {
private Long id;
private String quoteNumber;
private String companyName;
private String contactName;
private String email;
private String phone;
private String sector;
private Integer employeeCount;
private Long auditId;
private Double auditScore;
private List<QuoteModuleDTO> modules = new ArrayList<>();
private Double subtotalHT;
private Double discountPercentage;
private Double discountAmount;
private Double totalHT;
private Double vatRate;
private Double vatAmount;
private Double totalTTC;
private Double formationHours;
private Double formationRate;
private Double supportMonths;
private Double supportRate;
private String paymentTerms;
private String deliveryTerms;
private Integer validityDays;
private QuoteStatus status;
private LocalDateTime createdAt;
private LocalDateTime sentAt;
private LocalDateTime expiredAt;
// Constructeurs
public QuoteDTO() {}
public QuoteDTO(Quote quote) {
this.id = quote.getId();
this.quoteNumber = quote.getQuoteNumber();
this.companyName = quote.getCompanyName();
this.contactName = quote.getContactName();
this.email = quote.getEmail();
this.phone = quote.getPhone();
this.sector = quote.getSector();
this.employeeCount = quote.getEmployeeCount();
this.auditId = quote.getAuditId();
this.auditScore = quote.getAuditScore();
this.subtotalHT = quote.getSubtotalHT();
this.discountPercentage = quote.getDiscountPercentage();
this.discountAmount = quote.getDiscountAmount();
this.totalHT = quote.getTotalHT();
this.vatRate = quote.getVatRate();
this.vatAmount = quote.getVatAmount();
this.totalTTC = quote.getTotalTTC();
this.formationHours = quote.getFormationHours();
this.formationRate = quote.getFormationRate();
this.supportMonths = quote.getSupportMonths();
this.supportRate = quote.getSupportRate();
this.paymentTerms = quote.getPaymentTerms();
this.deliveryTerms = quote.getDeliveryTerms();
this.validityDays = quote.getValidityDays();
this.status = quote.getStatus();
this.createdAt = quote.getCreatedAt();
this.sentAt = quote.getSentAt();
this.expiredAt = quote.getExpiredAt();
// Conversion des modules
if (quote.getModules() != null) {
for (QuoteModule module : quote.getModules()) {
this.modules.add(new QuoteModuleDTO(module));
}
}
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getQuoteNumber() { return quoteNumber; }
public void setQuoteNumber(String quoteNumber) { this.quoteNumber = quoteNumber; }
public String getCompanyName() { return companyName; }
public void setCompanyName(String companyName) { this.companyName = companyName; }
public String getContactName() { return contactName; }
public void setContactName(String contactName) { this.contactName = contactName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getSector() { return sector; }
public void setSector(String sector) { this.sector = sector; }
public Integer getEmployeeCount() { return employeeCount; }
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
public Long getAuditId() { return auditId; }
public void setAuditId(Long auditId) { this.auditId = auditId; }
public Double getAuditScore() { return auditScore; }
public void setAuditScore(Double auditScore) { this.auditScore = auditScore; }
public List<QuoteModuleDTO> getModules() { return modules; }
public void setModules(List<QuoteModuleDTO> modules) { this.modules = modules; }
public Double getSubtotalHT() { return subtotalHT; }
public void setSubtotalHT(Double subtotalHT) { this.subtotalHT = subtotalHT; }
public Double getDiscountPercentage() { return discountPercentage; }
public void setDiscountPercentage(Double discountPercentage) { this.discountPercentage = discountPercentage; }
public Double getDiscountAmount() { return discountAmount; }
public void setDiscountAmount(Double discountAmount) { this.discountAmount = discountAmount; }
public Double getTotalHT() { return totalHT; }
public void setTotalHT(Double totalHT) { this.totalHT = totalHT; }
public Double getVatRate() { return vatRate; }
public void setVatRate(Double vatRate) { this.vatRate = vatRate; }
public Double getVatAmount() { return vatAmount; }
public void setVatAmount(Double vatAmount) { this.vatAmount = vatAmount; }
public Double getTotalTTC() { return totalTTC; }
public void setTotalTTC(Double totalTTC) { this.totalTTC = totalTTC; }
public Double getFormationHours() { return formationHours; }
public void setFormationHours(Double formationHours) { this.formationHours = formationHours; }
public Double getFormationRate() { return formationRate; }
public void setFormationRate(Double formationRate) { this.formationRate = formationRate; }
public Double getSupportMonths() { return supportMonths; }
public void setSupportMonths(Double supportMonths) { this.supportMonths = supportMonths; }
public Double getSupportRate() { return supportRate; }
public void setSupportRate(Double supportRate) { this.supportRate = supportRate; }
public String getPaymentTerms() { return paymentTerms; }
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
public String getDeliveryTerms() { return deliveryTerms; }
public void setDeliveryTerms(String deliveryTerms) { this.deliveryTerms = deliveryTerms; }
public Integer getValidityDays() { return validityDays; }
public void setValidityDays(Integer validityDays) { this.validityDays = validityDays; }
public QuoteStatus getStatus() { return status; }
public void setStatus(QuoteStatus status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getSentAt() { return sentAt; }
public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; }
public LocalDateTime getExpiredAt() { return expiredAt; }
public void setExpiredAt(LocalDateTime expiredAt) { this.expiredAt = expiredAt; }
}

View File

@@ -0,0 +1,115 @@
package dev.lions.quote;
import jakarta.persistence.*;
/**
* Module/ligne d'un devis
*/
@Entity
@Table(name = "quote_modules")
public class QuoteModule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "quote_id", nullable = false)
private Quote quote;
@Column(nullable = false)
private String moduleCode; // CRM, STOCK, COMPTA, RH, INFRA
@Column(nullable = false)
private String moduleName;
@Column(length = 1000)
private String description;
@Column(nullable = false)
private Double unitPrice; // Prix unitaire HT
@Column(nullable = false)
private Integer quantity = 1;
private String unit = "licence"; // licence, heure, mois, etc.
// Détails techniques
@Column(length = 2000)
private String technicalSpecs;
@Column(length = 1000)
private String deliverables;
private Integer implementationDays; // Jours d'implémentation
// Niveau de complexité basé sur l'audit
@Enumerated(EnumType.STRING)
private ComplexityLevel complexity = ComplexityLevel.STANDARD;
// Constructeurs
public QuoteModule() {}
public QuoteModule(String moduleCode, String moduleName, Double unitPrice) {
this.moduleCode = moduleCode;
this.moduleName = moduleName;
this.unitPrice = unitPrice;
}
// Méthodes métier
public Double getTotalPrice() {
return unitPrice * quantity;
}
public Double getComplexityMultiplier() {
return switch (complexity) {
case BASIC -> 0.8;
case STANDARD -> 1.0;
case ADVANCED -> 1.3;
case ENTERPRISE -> 1.6;
};
}
public Double getAdjustedPrice() {
return unitPrice * getComplexityMultiplier();
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Quote getQuote() { return quote; }
public void setQuote(Quote quote) { this.quote = quote; }
public String getModuleCode() { return moduleCode; }
public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; }
public String getModuleName() { return moduleName; }
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Double getUnitPrice() { return unitPrice; }
public void setUnitPrice(Double unitPrice) { this.unitPrice = unitPrice; }
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
public String getUnit() { return unit; }
public void setUnit(String unit) { this.unit = unit; }
public String getTechnicalSpecs() { return technicalSpecs; }
public void setTechnicalSpecs(String technicalSpecs) { this.technicalSpecs = technicalSpecs; }
public String getDeliverables() { return deliverables; }
public void setDeliverables(String deliverables) { this.deliverables = deliverables; }
public Integer getImplementationDays() { return implementationDays; }
public void setImplementationDays(Integer implementationDays) { this.implementationDays = implementationDays; }
public ComplexityLevel getComplexity() { return complexity; }
public void setComplexity(ComplexityLevel complexity) { this.complexity = complexity; }
}

View File

@@ -0,0 +1,70 @@
package dev.lions.quote;
/**
* DTO pour les modules de devis
*/
public class QuoteModuleDTO {
private Long id;
private String moduleCode;
private String moduleName;
private String description;
private Double unitPrice;
private Integer quantity;
private String unit;
private String technicalSpecs;
private String deliverables;
private Integer implementationDays;
private ComplexityLevel complexity;
// Constructeurs
public QuoteModuleDTO() {}
public QuoteModuleDTO(QuoteModule module) {
this.id = module.getId();
this.moduleCode = module.getModuleCode();
this.moduleName = module.getModuleName();
this.description = module.getDescription();
this.unitPrice = module.getUnitPrice();
this.quantity = module.getQuantity();
this.unit = module.getUnit();
this.technicalSpecs = module.getTechnicalSpecs();
this.deliverables = module.getDeliverables();
this.implementationDays = module.getImplementationDays();
this.complexity = module.getComplexity();
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getModuleCode() { return moduleCode; }
public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; }
public String getModuleName() { return moduleName; }
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public Double getUnitPrice() { return unitPrice; }
public void setUnitPrice(Double unitPrice) { this.unitPrice = unitPrice; }
public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
public String getUnit() { return unit; }
public void setUnit(String unit) { this.unit = unit; }
public String getTechnicalSpecs() { return technicalSpecs; }
public void setTechnicalSpecs(String technicalSpecs) { this.technicalSpecs = technicalSpecs; }
public String getDeliverables() { return deliverables; }
public void setDeliverables(String deliverables) { this.deliverables = deliverables; }
public Integer getImplementationDays() { return implementationDays; }
public void setImplementationDays(Integer implementationDays) { this.implementationDays = implementationDays; }
public ComplexityLevel getComplexity() { return complexity; }
public void setComplexity(ComplexityLevel complexity) { this.complexity = complexity; }
}

View File

@@ -0,0 +1,421 @@
package dev.lions.quote;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.Mailer;
import java.io.ByteArrayOutputStream;
import java.time.format.DateTimeFormatter;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Service de génération de rapports PDF pour les devis
*/
@ApplicationScoped
public class QuoteReportService {
@Inject
Mailer mailer;
private static final Font TITLE_FONT = new Font(Font.FontFamily.HELVETICA, 20, Font.BOLD, BaseColor.DARK_GRAY);
private static final Font HEADER_FONT = new Font(Font.FontFamily.HELVETICA, 14, Font.BOLD, BaseColor.BLACK);
private static final Font NORMAL_FONT = new Font(Font.FontFamily.HELVETICA, 11, Font.NORMAL, BaseColor.BLACK);
private static final Font SMALL_FONT = new Font(Font.FontFamily.HELVETICA, 9, Font.NORMAL, BaseColor.GRAY);
private static final Font PRICE_FONT = new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD, BaseColor.BLUE);
private final NumberFormat currencyFormat = NumberFormat.getInstance(Locale.FRANCE);
/**
* Génère le PDF du devis
*/
public byte[] generateQuotePDF(Quote quote) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
PdfWriter writer = PdfWriter.getInstance(document, baos);
document.open();
// En-tête Lions Dev
addHeader(document, quote);
// Informations client
addClientInfo(document, quote);
// Détail des modules
addModulesDetail(document, quote);
// Services additionnels
addAdditionalServices(document, quote);
// Récapitulatif financier
addFinancialSummary(document, quote);
// Conditions commerciales
addCommercialTerms(document, quote);
// Pied de page
addFooter(document);
document.close();
return baos.toByteArray();
}
private void addHeader(Document document, Quote quote) throws DocumentException {
// Logo et titre Lions Dev
PdfPTable headerTable = new PdfPTable(2);
headerTable.setWidthPercentage(100);
headerTable.setWidths(new float[]{2, 1});
// Colonne gauche - Lions Dev
PdfPCell leftCell = new PdfPCell();
leftCell.setBorder(Rectangle.NO_BORDER);
Paragraph title = new Paragraph("LIONS DEV", TITLE_FONT);
title.setSpacingAfter(5);
leftCell.addElement(title);
Paragraph subtitle = new Paragraph("Solutions Digitales Innovantes", HEADER_FONT);
subtitle.setSpacingAfter(10);
leftCell.addElement(subtitle);
Paragraph address = new Paragraph("Abidjan, Côte d'Ivoire\n+225 01 01 75 95 25\ncontact@lions.dev", NORMAL_FONT);
leftCell.addElement(address);
headerTable.addCell(leftCell);
// Colonne droite - Numéro devis
PdfPCell rightCell = new PdfPCell();
rightCell.setBorder(Rectangle.NO_BORDER);
rightCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
Paragraph quoteTitle = new Paragraph("DEVIS", new Font(Font.FontFamily.HELVETICA, 16, Font.BOLD));
quoteTitle.setAlignment(Element.ALIGN_RIGHT);
rightCell.addElement(quoteTitle);
Paragraph quoteNumber = new Paragraph(quote.getQuoteNumber(), HEADER_FONT);
quoteNumber.setAlignment(Element.ALIGN_RIGHT);
rightCell.addElement(quoteNumber);
Paragraph quoteDate = new Paragraph("Date: " + quote.getCreatedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")), NORMAL_FONT);
quoteDate.setAlignment(Element.ALIGN_RIGHT);
rightCell.addElement(quoteDate);
Paragraph validUntil = new Paragraph("Valide jusqu'au: " + quote.getExpiredAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")), NORMAL_FONT);
validUntil.setAlignment(Element.ALIGN_RIGHT);
rightCell.addElement(validUntil);
headerTable.addCell(rightCell);
document.add(headerTable);
document.add(Chunk.NEWLINE);
// Ligne de séparation
com.itextpdf.text.pdf.draw.LineSeparator line = new com.itextpdf.text.pdf.draw.LineSeparator();
document.add(new Chunk(line));
document.add(Chunk.NEWLINE);
}
private void addClientInfo(Document document, Quote quote) throws DocumentException {
Paragraph section = new Paragraph("INFORMATIONS CLIENT", HEADER_FONT);
section.setSpacingBefore(10);
section.setSpacingAfter(10);
document.add(section);
PdfPTable table = new PdfPTable(2);
table.setWidthPercentage(100);
table.setSpacingAfter(15);
addTableRow(table, "Entreprise:", quote.getCompanyName());
addTableRow(table, "Contact:", quote.getContactName());
addTableRow(table, "Email:", quote.getEmail());
addTableRow(table, "Téléphone:", quote.getPhone() != null ? quote.getPhone() : "-");
addTableRow(table, "Secteur:", quote.getSector() != null ? quote.getSector() : "-");
addTableRow(table, "Employés:", quote.getEmployeeCount() != null ? quote.getEmployeeCount() + " personnes" : "-");
if (quote.getAuditScore() != null) {
addTableRow(table, "Score audit:", String.format("%.1f%%", quote.getAuditScore()));
}
document.add(table);
}
private void addModulesDetail(Document document, Quote quote) throws DocumentException {
Paragraph section = new Paragraph("DÉTAIL DE LA SOLUTION", HEADER_FONT);
section.setSpacingBefore(15);
section.setSpacingAfter(10);
document.add(section);
if (quote.getModules().isEmpty()) {
Paragraph noModules = new Paragraph("Aucun module sélectionné", NORMAL_FONT);
document.add(noModules);
return;
}
PdfPTable table = new PdfPTable(5);
table.setWidthPercentage(100);
table.setWidths(new float[]{3, 1, 1, 1, 1.5f});
// En-têtes
addTableHeader(table, "Module / Description");
addTableHeader(table, "Qté");
addTableHeader(table, "Prix Unit.");
addTableHeader(table, "Niveau");
addTableHeader(table, "Total");
double subtotal = 0;
for (QuoteModule module : quote.getModules()) {
// Nom du module
PdfPCell nameCell = new PdfPCell(new Phrase(module.getModuleName(), NORMAL_FONT));
nameCell.setVerticalAlignment(Element.ALIGN_TOP);
table.addCell(nameCell);
// Quantité
table.addCell(new PdfPCell(new Phrase(module.getQuantity().toString(), NORMAL_FONT)));
// Prix unitaire
table.addCell(new PdfPCell(new Phrase(formatCurrency(module.getUnitPrice()), NORMAL_FONT)));
// Niveau de complexité
String complexityText = switch (module.getComplexity()) {
case BASIC -> "Basique";
case STANDARD -> "Standard";
case ADVANCED -> "Avancé";
case ENTERPRISE -> "Enterprise";
};
table.addCell(new PdfPCell(new Phrase(complexityText, NORMAL_FONT)));
// Total
double moduleTotal = module.getUnitPrice() * module.getQuantity();
table.addCell(new PdfPCell(new Phrase(formatCurrency(moduleTotal), PRICE_FONT)));
subtotal += moduleTotal;
// Description détaillée (ligne suivante)
if (module.getDescription() != null && !module.getDescription().isEmpty()) {
PdfPCell descCell = new PdfPCell(new Phrase(module.getDescription(), SMALL_FONT));
descCell.setColspan(5);
descCell.setPaddingTop(5);
descCell.setPaddingBottom(10);
table.addCell(descCell);
}
}
document.add(table);
}
private void addAdditionalServices(Document document, Quote quote) throws DocumentException {
if (quote.getFormationHours() > 0 || quote.getSupportMonths() > 0) {
Paragraph section = new Paragraph("SERVICES ADDITIONNELS", HEADER_FONT);
section.setSpacingBefore(15);
section.setSpacingAfter(10);
document.add(section);
PdfPTable table = new PdfPTable(4);
table.setWidthPercentage(100);
table.setWidths(new float[]{3, 1, 1, 1.5f});
// En-têtes
addTableHeader(table, "Service");
addTableHeader(table, "Quantité");
addTableHeader(table, "Prix Unit.");
addTableHeader(table, "Total");
// Formation
if (quote.getFormationHours() > 0) {
table.addCell(new PdfPCell(new Phrase("Formation utilisateurs", NORMAL_FONT)));
table.addCell(new PdfPCell(new Phrase(quote.getFormationHours() + " heures", NORMAL_FONT)));
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getFormationRate()), NORMAL_FONT)));
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getFormationHours() * quote.getFormationRate()), PRICE_FONT)));
}
// Support
if (quote.getSupportMonths() > 0) {
table.addCell(new PdfPCell(new Phrase("Support technique", NORMAL_FONT)));
table.addCell(new PdfPCell(new Phrase(quote.getSupportMonths() + " mois", NORMAL_FONT)));
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getSupportRate()), NORMAL_FONT)));
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getSupportMonths() * quote.getSupportRate()), PRICE_FONT)));
}
document.add(table);
}
}
private void addFinancialSummary(Document document, Quote quote) throws DocumentException {
Paragraph section = new Paragraph("RÉCAPITULATIF FINANCIER", HEADER_FONT);
section.setSpacingBefore(15);
section.setSpacingAfter(10);
document.add(section);
PdfPTable table = new PdfPTable(2);
table.setWidthPercentage(60);
table.setHorizontalAlignment(Element.ALIGN_RIGHT);
// Sous-total
addFinancialRow(table, "Sous-total HT:", formatCurrency(quote.getSubtotalHT()));
// Remise
if (quote.getDiscountPercentage() > 0) {
addFinancialRow(table, String.format("Remise (%.1f%%):", quote.getDiscountPercentage()),
"-" + formatCurrency(quote.getDiscountAmount()));
}
// Total HT
addFinancialRow(table, "Total HT:", formatCurrency(quote.getTotalHT()));
// TVA
addFinancialRow(table, String.format("TVA (%.0f%%):", quote.getVatRate()),
formatCurrency(quote.getVatAmount()));
// Total TTC
PdfPCell labelCell = new PdfPCell(new Phrase("TOTAL TTC:", new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD)));
labelCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
labelCell.setBorder(Rectangle.TOP);
table.addCell(labelCell);
PdfPCell valueCell = new PdfPCell(new Phrase(formatCurrency(quote.getTotalTTC()),
new Font(Font.FontFamily.HELVETICA, 14, Font.BOLD, BaseColor.BLUE)));
valueCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
valueCell.setBorder(Rectangle.TOP);
table.addCell(valueCell);
document.add(table);
}
private void addCommercialTerms(Document document, Quote quote) throws DocumentException {
Paragraph section = new Paragraph("CONDITIONS COMMERCIALES", HEADER_FONT);
section.setSpacingBefore(20);
section.setSpacingAfter(10);
document.add(section);
// Conditions de paiement
Paragraph payment = new Paragraph("Conditions de paiement: " + quote.getPaymentTerms(), NORMAL_FONT);
payment.setSpacingAfter(5);
document.add(payment);
// Délais de livraison
Paragraph delivery = new Paragraph("Délais de livraison: " + quote.getDeliveryTerms(), NORMAL_FONT);
delivery.setSpacingAfter(5);
document.add(delivery);
// Validité
Paragraph validity = new Paragraph("Validité du devis: " + quote.getValidityDays() + " jours", NORMAL_FONT);
validity.setSpacingAfter(10);
document.add(validity);
// Notes importantes
Paragraph notes = new Paragraph("Notes importantes:", HEADER_FONT);
notes.setSpacingAfter(5);
document.add(notes);
List notesList = new List(List.UNORDERED);
notesList.add(new ListItem("Prix exprimés en FCFA, hors taxes", SMALL_FONT));
notesList.add(new ListItem("Formation incluse sur site client", SMALL_FONT));
notesList.add(new ListItem("Support technique pendant la période indiquée", SMALL_FONT));
notesList.add(new ListItem("Garantie logiciel 12 mois", SMALL_FONT));
notesList.add(new ListItem("Mises à jour incluses pendant 6 mois", SMALL_FONT));
document.add(notesList);
}
private void addFooter(Document document) throws DocumentException {
Paragraph footer = new Paragraph("\nPour accepter ce devis, contactez-nous au +225 01 01 75 95 25\n" +
"ou par email à contact@lions.dev\n\n" +
"Lions Dev - Votre partenaire digital en Côte d'Ivoire", SMALL_FONT);
footer.setAlignment(Element.ALIGN_CENTER);
footer.setSpacingBefore(20);
document.add(footer);
}
/**
* Envoie le devis par email
*/
public void sendQuoteByEmail(Quote quote, String customMessage) {
try {
byte[] pdfBytes = generateQuotePDF(quote);
String emailContent = generateQuoteEmailContent(quote, customMessage);
Mail mail = Mail.withHtml(quote.getEmail(),
"Votre Devis Personnalisé - " + quote.getQuoteNumber(),
emailContent)
.addAttachment("devis-" + quote.getQuoteNumber() + ".pdf",
pdfBytes, "application/pdf");
mailer.send(mail);
} catch (Exception e) {
// Log l'erreur mais ne fait pas échouer le processus
System.err.println("Erreur envoi email devis: " + e.getMessage());
}
}
private String generateQuoteEmailContent(Quote quote, String customMessage) {
return String.format("""
<h2>Bonjour %s,</h2>
<p>Nous avons le plaisir de vous adresser votre devis personnalisé <strong>%s</strong>.</p>
%s
<h3>Récapitulatif:</h3>
<ul>
<li><strong>Montant total:</strong> %s FCFA TTC</li>
<li><strong>Validité:</strong> %d jours</li>
<li><strong>Délais:</strong> %s</li>
</ul>
<p>Ce devis a été établi suite à votre audit de maturité digitale (score: %.1f%%).</p>
<p><strong>Pour accepter ce devis:</strong></p>
<ul>
<li>Répondez à cet email</li>
<li>Appelez-nous au +225 01 01 75 95 25</li>
<li>Ou planifiez un rendez-vous sur notre site</li>
</ul>
<p>Notre équipe reste à votre disposition pour toute question ou personnalisation.</p>
<p>Cordialement,<br>
L'équipe Lions Dev<br>
+225 01 01 75 95 25<br>
contact@lions.dev</p>
""",
quote.getContactName(),
quote.getQuoteNumber(),
customMessage != null ? "<p>" + customMessage + "</p>" : "",
formatCurrency(quote.getTotalTTC()),
quote.getValidityDays(),
quote.getDeliveryTerms(),
quote.getAuditScore() != null ? quote.getAuditScore() : 0.0);
}
// Méthodes utilitaires
private void addTableRow(PdfPTable table, String label, String value) {
table.addCell(new PdfPCell(new Phrase(label, NORMAL_FONT)));
table.addCell(new PdfPCell(new Phrase(value != null ? value : "-", NORMAL_FONT)));
}
private void addTableHeader(PdfPTable table, String header) {
PdfPCell cell = new PdfPCell(new Phrase(header, HEADER_FONT));
cell.setBackgroundColor(BaseColor.LIGHT_GRAY);
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
table.addCell(cell);
}
private void addFinancialRow(PdfPTable table, String label, String value) {
PdfPCell labelCell = new PdfPCell(new Phrase(label, NORMAL_FONT));
labelCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
table.addCell(labelCell);
PdfPCell valueCell = new PdfPCell(new Phrase(value, PRICE_FONT));
valueCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
table.addCell(valueCell);
}
private String formatCurrency(Double amount) {
if (amount == null) return "0";
return String.format("%,.0f", amount) + " FCFA";
}
}

View File

@@ -0,0 +1,296 @@
package dev.lions.quote;
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.stream.Collectors;
/**
* API REST pour la gestion des devis
*/
@Path("/api/quotes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class QuoteResource {
@Inject
QuoteService quoteService;
@Inject
QuoteReportService quoteReportService;
/**
* Génère un devis automatique à partir d'un audit
*/
@POST
@Path("/generate/{auditId}")
public Response generateQuoteFromAudit(@PathParam("auditId") Long auditId) {
try {
Quote quote = quoteService.generateQuoteFromAudit(auditId);
QuoteDTO quoteDTO = new QuoteDTO(quote);
return Response.ok(quoteDTO).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la génération du devis"))
.build();
}
}
/**
* Récupère un devis par ID
*/
@GET
@Path("/{quoteId}")
public Response getQuote(@PathParam("quoteId") Long quoteId) {
Quote quote = quoteService.getQuoteById(quoteId);
if (quote == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Devis non trouvé"))
.build();
}
QuoteDTO quoteDTO = new QuoteDTO(quote);
return Response.ok(quoteDTO).build();
}
/**
* Personnalise un devis existant
*/
@PUT
@Path("/{quoteId}/customize")
public Response customizeQuote(@PathParam("quoteId") Long quoteId,
QuoteCustomizationDTO customization) {
try {
Quote quote = quoteService.customizeQuote(quoteId, customization);
QuoteDTO quoteDTO = new QuoteDTO(quote);
return Response.ok(quoteDTO).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", e.getMessage()))
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la personnalisation"))
.build();
}
}
/**
* Applique une remise à un devis
*/
@PUT
@Path("/{quoteId}/discount")
public Response applyDiscount(@PathParam("quoteId") Long quoteId,
Map<String, Object> discountData) {
try {
Double percentage = ((Number) discountData.get("percentage")).doubleValue();
String reason = (String) discountData.get("reason");
quoteService.applyDiscount(quoteId, percentage, reason);
Quote quote = quoteService.getQuoteById(quoteId);
QuoteDTO quoteDTO = new QuoteDTO(quote);
return Response.ok(quoteDTO).build();
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Erreur lors de l'application de la remise"))
.build();
}
}
/**
* Met à jour le statut d'un devis
*/
@PUT
@Path("/{quoteId}/status")
public Response updateStatus(@PathParam("quoteId") Long quoteId,
Map<String, String> statusData) {
try {
QuoteStatus status = QuoteStatus.valueOf(statusData.get("status"));
quoteService.updateQuoteStatus(quoteId, status);
return Response.ok(Map.of("message", "Statut mis à jour")).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Statut invalide"))
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la mise à jour"))
.build();
}
}
/**
* Récupère la liste des devis en attente
*/
@GET
@Path("/pending")
public Response getPendingQuotes() {
List<Quote> quotes = quoteService.getPendingQuotes();
List<QuoteDTO> quoteDTOs = quotes.stream()
.map(QuoteDTO::new)
.collect(Collectors.toList());
return Response.ok(quoteDTOs).build();
}
/**
* Récupère le catalogue des modules
*/
@GET
@Path("/catalog")
public Response getModuleCatalog() {
List<ModuleCatalog> catalog = quoteService.getActiveCatalog();
return Response.ok(catalog).build();
}
/**
* Génère le PDF du devis
*/
@GET
@Path("/{quoteId}/pdf")
@Produces("application/pdf")
public Response generateQuotePDF(@PathParam("quoteId") Long quoteId) {
try {
Quote quote = quoteService.getQuoteById(quoteId);
if (quote == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Devis non trouvé"))
.build();
}
byte[] pdfBytes = quoteReportService.generateQuotePDF(quote);
return Response.ok(pdfBytes)
.header("Content-Disposition",
"attachment; filename=\"devis-" + quote.getQuoteNumber() + ".pdf\"")
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la génération du PDF"))
.build();
}
}
/**
* Envoie le devis par email
*/
@POST
@Path("/{quoteId}/send")
public Response sendQuote(@PathParam("quoteId") Long quoteId,
Map<String, String> emailData) {
try {
Quote quote = quoteService.getQuoteById(quoteId);
if (quote == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Devis non trouvé"))
.build();
}
String message = emailData.get("message");
quoteReportService.sendQuoteByEmail(quote, message);
// Mise à jour du statut
quoteService.updateQuoteStatus(quoteId, QuoteStatus.SENT);
return Response.ok(Map.of("message", "Devis envoyé avec succès")).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de l'envoi"))
.build();
}
}
/**
* Statistiques des devis
*/
@GET
@Path("/statistics")
public Response getStatistics() {
Map<String, Object> stats = quoteService.getQuoteStatistics();
return Response.ok(stats).build();
}
/**
* Recherche de devis
*/
@GET
@Path("/search")
public Response searchQuotes(@QueryParam("company") String company,
@QueryParam("status") String status,
@QueryParam("from") String fromDate,
@QueryParam("to") String toDate) {
// Implémentation de la recherche
// Pour l'instant, retourne tous les devis en attente
List<Quote> quotes = quoteService.getPendingQuotes();
List<QuoteDTO> quoteDTOs = quotes.stream()
.map(QuoteDTO::new)
.collect(Collectors.toList());
return Response.ok(quoteDTOs).build();
}
/**
* Acceptation d'un devis par le client (lien public)
*/
@POST
@Path("/{quoteId}/accept")
public Response acceptQuote(@PathParam("quoteId") Long quoteId,
Map<String, String> acceptanceData) {
try {
Quote quote = quoteService.getQuoteById(quoteId);
if (quote == null || !quote.isValid()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Devis non valide ou expiré"))
.build();
}
// Mise à jour du statut et feedback client
quoteService.updateQuoteStatus(quoteId, QuoteStatus.ACCEPTED);
String feedback = acceptanceData.get("feedback");
if (feedback != null) {
quote.setClientFeedback(feedback);
}
return Response.ok(Map.of("message", "Devis accepté avec succès")).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de l'acceptation"))
.build();
}
}
/**
* Consultation d'un devis par le client (lien public)
*/
@GET
@Path("/{quoteId}/view")
public Response viewQuote(@PathParam("quoteId") Long quoteId) {
Quote quote = quoteService.getQuoteById(quoteId);
if (quote == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Devis non trouvé"))
.build();
}
// Marquer comme consulté
if (quote.getStatus() == QuoteStatus.SENT) {
quoteService.updateQuoteStatus(quoteId, QuoteStatus.VIEWED);
}
QuoteDTO quoteDTO = new QuoteDTO(quote);
return Response.ok(quoteDTO).build();
}
}

View File

@@ -0,0 +1,356 @@
package dev.lions.quote;
import dev.lions.audit.AuditResponse;
import dev.lions.audit.AuditService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service de gestion des devis personnalisés
*/
@ApplicationScoped
public class QuoteService {
@Inject
EntityManager em;
@Inject
AuditService auditService;
/**
* Génère un devis automatique basé sur un audit
*/
@Transactional
public Quote generateQuoteFromAudit(Long auditId) {
AuditResponse audit = auditService.getAuditById(auditId);
if (audit == null) {
throw new IllegalArgumentException("Audit non trouvé");
}
Quote quote = new Quote();
quote.setCompanyName(audit.getCompanyName());
quote.setContactName(audit.getContactName());
quote.setEmail(audit.getEmail());
quote.setPhone(audit.getPhone());
quote.setSector(audit.getSector());
quote.setEmployeeCount(audit.getEmployeeCount());
quote.setAuditId(auditId);
quote.setAuditScore(audit.getMaturityPercentage());
// Génération du numéro de devis
quote.setQuoteNumber(generateQuoteNumber());
// Sélection automatique des modules selon l'audit
List<QuoteModule> recommendedModules = recommendModulesFromAudit(audit);
for (QuoteModule module : recommendedModules) {
module.setQuote(quote);
quote.getModules().add(module);
}
// Calcul des services additionnels
calculateAdditionalServices(quote, audit);
// Calcul des totaux
quote.calculateTotals();
// Sauvegarde
em.persist(quote);
return quote;
}
/**
* Recommande les modules selon les résultats d'audit
*/
private List<QuoteModule> recommendModulesFromAudit(AuditResponse audit) {
List<QuoteModule> modules = new ArrayList<>();
Map<String, Integer> categoryScores = audit.getCategoryScores();
// Récupération du catalogue
List<ModuleCatalog> catalog = getActiveCatalog();
for (Map.Entry<String, Integer> entry : categoryScores.entrySet()) {
String category = entry.getKey();
Integer score = entry.getValue();
// Si le score est faible, recommander le module
if (score < 60) { // Seuil configurable
ModuleCatalog catalogModule = findCatalogModule(catalog, category);
if (catalogModule != null) {
QuoteModule module = createModuleFromCatalog(catalogModule, audit);
modules.add(module);
}
}
}
return modules;
}
/**
* Crée un module de devis à partir du catalogue
*/
private QuoteModule createModuleFromCatalog(ModuleCatalog catalogModule, AuditResponse audit) {
QuoteModule module = new QuoteModule();
module.setModuleCode(catalogModule.getModuleCode());
module.setModuleName(catalogModule.getModuleName());
module.setDescription(catalogModule.getDescription());
module.setTechnicalSpecs(catalogModule.getTechnicalRequirements());
// Détermination du niveau de complexité
ComplexityLevel complexity = catalogModule.getRecommendedComplexity(
audit.getMaturityPercentage(),
audit.getEmployeeCount()
);
module.setComplexity(complexity);
// Prix selon la complexité
module.setUnitPrice(catalogModule.getPriceForComplexity(complexity));
module.setImplementationDays(catalogModule.getEstimatedImplementationDays(complexity));
// Génération des livrables
module.setDeliverables(generateDeliverables(catalogModule, complexity));
return module;
}
/**
* Calcule les services additionnels (formation, support)
*/
private void calculateAdditionalServices(Quote quote, AuditResponse audit) {
// Formation : basée sur le nombre d'employés et la complexité
int employeeCount = audit.getEmployeeCount() != null ? audit.getEmployeeCount() : 5;
double formationHours = Math.max(8, employeeCount * 2); // Min 8h, 2h par employé
// Ajustement selon le score d'audit (plus le score est bas, plus de formation)
if (audit.getMaturityPercentage() < 30) {
formationHours *= 1.5;
} else if (audit.getMaturityPercentage() < 60) {
formationHours *= 1.2;
}
quote.setFormationHours(formationHours);
// Support : 3 mois minimum, plus selon la complexité
double supportMonths = 3.0;
if (quote.getModules().size() > 2) {
supportMonths = 6.0;
}
if (quote.getModules().size() > 4) {
supportMonths = 12.0;
}
quote.setSupportMonths(supportMonths);
}
/**
* Génère un numéro de devis unique
*/
private String generateQuoteNumber() {
int year = LocalDateTime.now().getYear();
// Récupération du dernier numéro de l'année
String lastNumber = em.createQuery(
"SELECT q.quoteNumber FROM Quote q WHERE q.quoteNumber LIKE :pattern ORDER BY q.quoteNumber DESC",
String.class)
.setParameter("pattern", "QUO-" + year + "-%")
.setMaxResults(1)
.getResultStream()
.findFirst()
.orElse("QUO-" + year + "-000");
// Extraction et incrémentation du numéro
String[] parts = lastNumber.split("-");
int nextNumber = Integer.parseInt(parts[2]) + 1;
return String.format("QUO-%d-%03d", year, nextNumber);
}
/**
* Récupère le catalogue actif
*/
public List<ModuleCatalog> getActiveCatalog() {
return em.createQuery(
"SELECT m FROM ModuleCatalog m WHERE m.active = true ORDER BY m.category, m.displayOrder",
ModuleCatalog.class
).getResultList();
}
/**
* Trouve un module du catalogue par catégorie
*/
private ModuleCatalog findCatalogModule(List<ModuleCatalog> catalog, String category) {
return catalog.stream()
.filter(m -> category.equals(m.getCategory()))
.findFirst()
.orElse(null);
}
/**
* Génère les livrables selon le module et la complexité
*/
private String generateDeliverables(ModuleCatalog catalogModule, ComplexityLevel complexity) {
StringBuilder deliverables = new StringBuilder();
deliverables.append("• Installation et configuration du module ").append(catalogModule.getModuleName()).append("\n");
deliverables.append("• Paramétrage selon vos processus métier\n");
deliverables.append("• Formation des utilisateurs\n");
deliverables.append("• Documentation utilisateur\n");
if (complexity == ComplexityLevel.ADVANCED || complexity == ComplexityLevel.ENTERPRISE) {
deliverables.append("• Intégrations avec systèmes existants\n");
deliverables.append("• Rapports personnalisés\n");
}
if (complexity == ComplexityLevel.ENTERPRISE) {
deliverables.append("• Workflow avancés\n");
deliverables.append("• API personnalisées\n");
deliverables.append("• Support prioritaire\n");
}
deliverables.append("• Garantie 12 mois\n");
return deliverables.toString();
}
/**
* Récupère un devis par ID
*/
public Quote getQuoteById(Long quoteId) {
return em.find(Quote.class, quoteId);
}
/**
* Met à jour le statut d'un devis
*/
@Transactional
public void updateQuoteStatus(Long quoteId, QuoteStatus status) {
Quote quote = em.find(Quote.class, quoteId);
if (quote != null) {
quote.setStatus(status);
switch (status) {
case SENT -> quote.setSentAt(LocalDateTime.now());
case VIEWED -> quote.setViewedAt(LocalDateTime.now());
case ACCEPTED -> quote.setAcceptedAt(LocalDateTime.now());
}
}
}
/**
* Applique une remise à un devis
*/
@Transactional
public void applyDiscount(Long quoteId, Double discountPercentage, String reason) {
Quote quote = em.find(Quote.class, quoteId);
if (quote != null) {
quote.setDiscountPercentage(discountPercentage);
quote.calculateTotals();
String note = String.format("Remise appliquée: %.1f%% - Raison: %s",
discountPercentage, reason);
quote.setSalesNotes(quote.getSalesNotes() != null ?
quote.getSalesNotes() + "\n" + note : note);
}
}
/**
* Récupère les devis en attente
*/
public List<Quote> getPendingQuotes() {
return em.createQuery(
"SELECT q FROM Quote q WHERE q.status IN :statuses ORDER BY q.createdAt DESC",
Quote.class)
.setParameter("statuses", Arrays.asList(QuoteStatus.SENT, QuoteStatus.VIEWED))
.getResultList();
}
/**
* Statistiques des devis
*/
public Map<String, Object> getQuoteStatistics() {
Map<String, Object> stats = new HashMap<>();
// Nombre total de devis
Long totalQuotes = em.createQuery("SELECT COUNT(q) FROM Quote q", Long.class)
.getSingleResult();
stats.put("totalQuotes", totalQuotes);
// Taux de conversion
Long acceptedQuotes = em.createQuery(
"SELECT COUNT(q) FROM Quote q WHERE q.status = :status", Long.class)
.setParameter("status", QuoteStatus.ACCEPTED)
.getSingleResult();
double conversionRate = totalQuotes > 0 ? (double) acceptedQuotes / totalQuotes * 100 : 0;
stats.put("conversionRate", conversionRate);
// Montant total des devis acceptés
Double totalRevenue = em.createQuery(
"SELECT SUM(q.totalTTC) FROM Quote q WHERE q.status = :status", Double.class)
.setParameter("status", QuoteStatus.ACCEPTED)
.getSingleResult();
stats.put("totalRevenue", totalRevenue != null ? totalRevenue : 0.0);
// Ticket moyen
double averageTicket = acceptedQuotes > 0 ? totalRevenue / acceptedQuotes : 0;
stats.put("averageTicket", averageTicket);
return stats;
}
/**
* Personnalise un devis existant
*/
@Transactional
public Quote customizeQuote(Long quoteId, QuoteCustomizationDTO customization) {
Quote quote = em.find(Quote.class, quoteId);
if (quote == null) {
throw new IllegalArgumentException("Devis non trouvé");
}
// Mise à jour des modules
if (customization.getModules() != null) {
quote.getModules().clear();
for (QuoteModuleDTO moduleDTO : customization.getModules()) {
QuoteModule module = new QuoteModule();
module.setQuote(quote);
module.setModuleCode(moduleDTO.getModuleCode());
module.setModuleName(moduleDTO.getModuleName());
module.setDescription(moduleDTO.getDescription());
module.setUnitPrice(moduleDTO.getUnitPrice());
module.setQuantity(moduleDTO.getQuantity());
module.setComplexity(moduleDTO.getComplexity());
quote.getModules().add(module);
}
}
// Mise à jour des services
if (customization.getFormationHours() != null) {
quote.setFormationHours(customization.getFormationHours());
}
if (customization.getSupportMonths() != null) {
quote.setSupportMonths(customization.getSupportMonths());
}
// Mise à jour des conditions
if (customization.getPaymentTerms() != null) {
quote.setPaymentTerms(customization.getPaymentTerms());
}
if (customization.getDeliveryTerms() != null) {
quote.setDeliveryTerms(customization.getDeliveryTerms());
}
// Recalcul des totaux
quote.calculateTotals();
return quote;
}
}

View File

@@ -1,264 +1,264 @@
package dev.lions.repositories; package dev.lions.repositories;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Root;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import dev.lions.events.AnalyticsEvent; import dev.lions.events.AnalyticsEvent;
import dev.lions.exceptions.RepositoryException; import dev.lions.exceptions.RepositoryException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Repository gérant la persistance des événements analytiques. * Repository gérant la persistance des événements analytiques.
* Cette classe assure le stockage, la récupération et l'analyse * Cette classe assure le stockage, la récupération et l'analyse
* des événements analytiques de l'application. * des événements analytiques de l'application.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.1 * @version 1.1
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class AnalyticsRepository extends BaseRepository<AnalyticsEvent, Long> { public class AnalyticsRepository extends BaseRepository<AnalyticsEvent, Long> {
@PersistenceContext @PersistenceContext
private EntityManager entityManager; private EntityManager entityManager;
/** /**
* Recherche tous les événements pour une période donnée. * Recherche tous les événements pour une période donnée.
* *
* @param startDate Date de début de la période * @param startDate Date de début de la période
* @param endDate Date de fin de la période * @param endDate Date de fin de la période
* @return Liste des événements trouvés * @return Liste des événements trouvés
* @throws RepositoryException En cas d'erreur de requête * @throws RepositoryException En cas d'erreur de requête
*/ */
public List<AnalyticsEvent> findEventsByDateRange(@NotNull LocalDateTime startDate, public List<AnalyticsEvent> findEventsByDateRange(@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) { @NotNull LocalDateTime endDate) {
log.debug("Recherche des événements entre {} et {}", startDate, endDate); log.debug("Recherche des événements entre {} et {}", startDate, endDate);
try { try {
TypedQuery<AnalyticsEvent> query = entityManager.createQuery( TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
"SELECT e FROM AnalyticsEvent e " + "SELECT e FROM AnalyticsEvent e " +
"WHERE e.timestamp BETWEEN :startDate AND :endDate " + "WHERE e.timestamp BETWEEN :startDate AND :endDate " +
"ORDER BY e.timestamp DESC", "ORDER BY e.timestamp DESC",
AnalyticsEvent.class); AnalyticsEvent.class);
query.setParameter("startDate", startDate); query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate); query.setParameter("endDate", endDate);
List<AnalyticsEvent> events = query.getResultList(); List<AnalyticsEvent> events = query.getResultList();
log.info("Trouvé {} événements pour la période demandée", events.size()); log.info("Trouvé {} événements pour la période demandée", events.size());
return events; return events;
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de la recherche des événements par période", e); log.error("Erreur lors de la recherche des événements par période", e);
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des événements par période", e); "Erreur lors de la recherche des événements par période", e);
} }
} }
/** /**
* Recherche les événements analytiques par type pour une période donnée. * Recherche les événements analytiques par type pour une période donnée.
* *
* @param eventType Type d'événement recherché * @param eventType Type d'événement recherché
* @param startDate Date de début * @param startDate Date de début
* @param endDate Date de fin * @param endDate Date de fin
* @return Liste des événements trouvés * @return Liste des événements trouvés
*/ */
public List<AnalyticsEvent> findEventsByType(@NotNull String eventType, public List<AnalyticsEvent> findEventsByType(@NotNull String eventType,
@NotNull LocalDateTime startDate, @NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) { @NotNull LocalDateTime endDate) {
log.debug("Recherche des événements de type {} entre {} et {}", log.debug("Recherche des événements de type {} entre {} et {}",
eventType, startDate, endDate); eventType, startDate, endDate);
try { try {
TypedQuery<AnalyticsEvent> query = entityManager.createQuery( TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
"SELECT e FROM AnalyticsEvent e " + "SELECT e FROM AnalyticsEvent e " +
"WHERE e.eventType = :eventType " + "WHERE e.eventType = :eventType " +
"AND e.timestamp BETWEEN :startDate AND :endDate " + "AND e.timestamp BETWEEN :startDate AND :endDate " +
"ORDER BY e.timestamp DESC", "ORDER BY e.timestamp DESC",
AnalyticsEvent.class); AnalyticsEvent.class);
query.setParameter("eventType", eventType); query.setParameter("eventType", eventType);
query.setParameter("startDate", startDate); query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate); query.setParameter("endDate", endDate);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des événements par type", e); "Erreur lors de la recherche des événements par type", e);
} }
} }
/** /**
* Enregistre un nouvel événement analytique. * Enregistre un nouvel événement analytique.
* *
* @param event Événement à sauvegarder * @param event Événement à sauvegarder
* @return Événement sauvegardé * @return Événement sauvegardé
*/ */
@Transactional @Transactional
public AnalyticsEvent save(AnalyticsEvent event) { public AnalyticsEvent save(AnalyticsEvent event) {
log.debug("Sauvegarde d'un nouvel événement de type: {}", event.getEventType()); log.debug("Sauvegarde d'un nouvel événement de type: {}", event.getEventType());
try { try {
if (event.getId() == null) { if (event.getId() == null) {
entityManager.persist(event); entityManager.persist(event);
log.info("Nouvel événement créé avec l'ID: {}", event.getId()); log.info("Nouvel événement créé avec l'ID: {}", event.getId());
} else { } else {
event = entityManager.merge(event); event = entityManager.merge(event);
log.info("Événement mis à jour avec l'ID: {}", event.getId()); log.info("Événement mis à jour avec l'ID: {}", event.getId());
} }
return event; return event;
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de la sauvegarde de l'événement", e); log.error("Erreur lors de la sauvegarde de l'événement", e);
throw new RepositoryException("Erreur lors de la sauvegarde de l'événement", e); throw new RepositoryException("Erreur lors de la sauvegarde de l'événement", e);
} }
} }
/** /**
* Calcule le nombre d'événements par type pour une période donnée. * Calcule le nombre d'événements par type pour une période donnée.
* *
* @param startDate Date de début * @param startDate Date de début
* @param endDate Date de fin * @param endDate Date de fin
* @return Map des compteurs par type d'événement * @return Map des compteurs par type d'événement
*/ */
public Map<String, Long> getEventCountByType(@NotNull LocalDateTime startDate, public Map<String, Long> getEventCountByType(@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) { @NotNull LocalDateTime endDate) {
log.debug("Calcul du nombre d'événements entre {} et {}", startDate, endDate); log.debug("Calcul du nombre d'événements entre {} et {}", startDate, endDate);
try { try {
List<Object[]> results = entityManager.createQuery( List<Object[]> results = entityManager.createQuery(
"SELECT e.eventType, COUNT(e) FROM AnalyticsEvent e " + "SELECT e.eventType, COUNT(e) FROM AnalyticsEvent e " +
"WHERE e.timestamp BETWEEN :startDate AND :endDate " + "WHERE e.timestamp BETWEEN :startDate AND :endDate " +
"GROUP BY e.eventType ORDER BY COUNT(e) DESC", "GROUP BY e.eventType ORDER BY COUNT(e) DESC",
Object[].class) Object[].class)
.setParameter("startDate", startDate) .setParameter("startDate", startDate)
.setParameter("endDate", endDate) .setParameter("endDate", endDate)
.getResultList(); .getResultList();
return results.stream() return results.stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
row -> (String) row[0], row -> (String) row[0],
row -> (Long) row[1] row -> (Long) row[1]
)); ));
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors du calcul du nombre d'événements", e); "Erreur lors du calcul du nombre d'événements", e);
} }
} }
/** /**
* Recherche les événements associés à un contact spécifique. * Recherche les événements associés à un contact spécifique.
* *
* @param contactId Identifiant du contact * @param contactId Identifiant du contact
* @return Liste des événements associés * @return Liste des événements associés
*/ */
public List<AnalyticsEvent> findEventsByContactId(@NotNull String contactId) { public List<AnalyticsEvent> findEventsByContactId(@NotNull String contactId) {
log.debug("Recherche des événements pour le contact {}", contactId); log.debug("Recherche des événements pour le contact {}", contactId);
try { try {
TypedQuery<AnalyticsEvent> query = entityManager.createQuery( TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
"SELECT e FROM AnalyticsEvent e " + "SELECT e FROM AnalyticsEvent e " +
"WHERE e.contactId = :contactId " + "WHERE e.contactId = :contactId " +
"ORDER BY e.timestamp DESC", "ORDER BY e.timestamp DESC",
AnalyticsEvent.class); AnalyticsEvent.class);
query.setParameter("contactId", contactId); query.setParameter("contactId", contactId);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des événements par contact", e); "Erreur lors de la recherche des événements par contact", e);
} }
} }
/** /**
* Supprime les événements antérieurs à une date donnée. * Supprime les événements antérieurs à une date donnée.
* *
* @param retentionDate Date limite de conservation * @param retentionDate Date limite de conservation
* @return Nombre d'événements supprimés * @return Nombre d'événements supprimés
*/ */
@Transactional @Transactional
public int deleteEventsOlderThan(@NotNull LocalDateTime retentionDate) { public int deleteEventsOlderThan(@NotNull LocalDateTime retentionDate) {
log.info("Suppression des événements antérieurs à {}", retentionDate); log.info("Suppression des événements antérieurs à {}", retentionDate);
try { try {
int deletedCount = entityManager.createQuery( int deletedCount = entityManager.createQuery(
"DELETE FROM AnalyticsEvent e WHERE e.timestamp < :retentionDate") "DELETE FROM AnalyticsEvent e WHERE e.timestamp < :retentionDate")
.setParameter("retentionDate", retentionDate) .setParameter("retentionDate", retentionDate)
.executeUpdate(); .executeUpdate();
log.info("{} événements supprimés", deletedCount); log.info("{} événements supprimés", deletedCount);
return deletedCount; return deletedCount;
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la suppression des anciens événements", e); "Erreur lors de la suppression des anciens événements", e);
} }
} }
/** /**
* Recherche un événement par son identifiant. * Recherche un événement par son identifiant.
* *
* @param id Identifiant de l'événement * @param id Identifiant de l'événement
* @return Optional contenant l'événement s'il existe * @return Optional contenant l'événement s'il existe
*/ */
public Optional<AnalyticsEvent> findById(Long id) { public Optional<AnalyticsEvent> findById(Long id) {
log.debug("Recherche de l'événement avec l'ID: {}", id); log.debug("Recherche de l'événement avec l'ID: {}", id);
try { try {
AnalyticsEvent event = entityManager.find(AnalyticsEvent.class, id); AnalyticsEvent event = entityManager.find(AnalyticsEvent.class, id);
return Optional.ofNullable(event); return Optional.ofNullable(event);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche de l'événement par ID", e); "Erreur lors de la recherche de l'événement par ID", e);
} }
} }
/** /**
* Calcule les statistiques d'événements par environnement. * Calcule les statistiques d'événements par environnement.
* *
* @param startDate Date de début * @param startDate Date de début
* @param endDate Date de fin * @param endDate Date de fin
* @return Map des statistiques par environnement * @return Map des statistiques par environnement
*/ */
public Map<String, Long> getEventStatistics(LocalDateTime startDate, public Map<String, Long> getEventStatistics(LocalDateTime startDate,
LocalDateTime endDate) { LocalDateTime endDate) {
log.debug("Calcul des statistiques entre {} et {}", startDate, endDate); log.debug("Calcul des statistiques entre {} et {}", startDate, endDate);
try { try {
List<Object[]> results = entityManager.createQuery( List<Object[]> results = entityManager.createQuery(
"SELECT e.environment, COUNT(e) FROM AnalyticsEvent e " + "SELECT e.environment, COUNT(e) FROM AnalyticsEvent e " +
"WHERE e.timestamp BETWEEN :startDate AND :endDate " + "WHERE e.timestamp BETWEEN :startDate AND :endDate " +
"GROUP BY e.environment", "GROUP BY e.environment",
Object[].class) Object[].class)
.setParameter("startDate", startDate) .setParameter("startDate", startDate)
.setParameter("endDate", endDate) .setParameter("endDate", endDate)
.getResultList(); .getResultList();
return results.stream() return results.stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
row -> (String) row[0], row -> (String) row[0],
row -> (Long) row[1] row -> (Long) row[1]
)); ));
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors du calcul des statistiques", e); "Erreur lors du calcul des statistiques", e);
} }
} }
} }

View File

@@ -1,194 +1,194 @@
package dev.lions.repositories; package dev.lions.repositories;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import dev.lions.exceptions.RepositoryException; import dev.lions.exceptions.RepositoryException;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
* Repository générique fournissant les opérations CRUD de base. * Repository générique fournissant les opérations CRUD de base.
* Cette classe abstract implémente les fonctionnalités communes à tous les repositories * Cette classe abstract implémente les fonctionnalités communes à tous les repositories
* de l'application en assurant une gestion cohérente des entités. * de l'application en assurant une gestion cohérente des entités.
* *
* @param <T> Type de l'entité * @param <T> Type de l'entité
* @param <ID> Type de l'identifiant de l'entité * @param <ID> Type de l'identifiant de l'entité
*/ */
@Slf4j @Slf4j
public abstract class BaseRepository<T, ID> { public abstract class BaseRepository<T, ID> {
@PersistenceContext @PersistenceContext
protected EntityManager entityManager; protected EntityManager entityManager;
private final Class<T> entityClass; private final Class<T> entityClass;
/** /**
* Constructeur initialisant la classe d'entité via réflexion. * Constructeur initialisant la classe d'entité via réflexion.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public BaseRepository() { public BaseRepository() {
Class<?> currentClass = getClass(); Class<?> currentClass = getClass();
while (!(currentClass.getGenericSuperclass() instanceof ParameterizedType)) { while (!(currentClass.getGenericSuperclass() instanceof ParameterizedType)) {
currentClass = currentClass.getSuperclass(); currentClass = currentClass.getSuperclass();
} }
this.entityClass = (Class<T>) ((ParameterizedType) currentClass.getGenericSuperclass()) this.entityClass = (Class<T>) ((ParameterizedType) currentClass.getGenericSuperclass())
.getActualTypeArguments()[0]; .getActualTypeArguments()[0];
log.debug("Repository initialisé pour l'entité : {}", entityClass.getSimpleName()); log.debug("Repository initialisé pour l'entité : {}", entityClass.getSimpleName());
} }
/** /**
* Persiste une nouvelle entité. * Persiste une nouvelle entité.
* *
* @param entity Entité à persister * @param entity Entité à persister
* @return Entité persistée * @return Entité persistée
*/ */
@Transactional @Transactional
public T save(T entity) { public T save(T entity) {
try { try {
log.debug("Sauvegarde d'une nouvelle entité : {}", entityClass.getSimpleName()); log.debug("Sauvegarde d'une nouvelle entité : {}", entityClass.getSimpleName());
entityManager.persist(entity); entityManager.persist(entity);
entityManager.flush(); entityManager.flush();
log.info("Entité sauvegardée avec succès : {}", entity); log.info("Entité sauvegardée avec succès : {}", entity);
return entity; return entity;
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException("Erreur lors de la sauvegarde de l'entité", e); throw new RepositoryException("Erreur lors de la sauvegarde de l'entité", e);
} }
} }
/** /**
* Met à jour une entité existante. * Met à jour une entité existante.
* *
* @param entity Entité à mettre à jour * @param entity Entité à mettre à jour
* @return Entité mise à jour * @return Entité mise à jour
*/ */
@Transactional @Transactional
public T update(T entity) { public T update(T entity) {
try { try {
log.debug("Mise à jour d'une entité : {}", entityClass.getSimpleName()); log.debug("Mise à jour d'une entité : {}", entityClass.getSimpleName());
T updatedEntity = entityManager.merge(entity); T updatedEntity = entityManager.merge(entity);
entityManager.flush(); entityManager.flush();
log.info("Entité mise à jour avec succès : {}", entity); log.info("Entité mise à jour avec succès : {}", entity);
return updatedEntity; return updatedEntity;
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException("Erreur lors de la mise à jour de l'entité", e); throw new RepositoryException("Erreur lors de la mise à jour de l'entité", e);
} }
} }
/** /**
* Recherche une entité par son identifiant. * Recherche une entité par son identifiant.
* *
* @param id Identifiant de l'entité * @param id Identifiant de l'entité
* @return Entité trouvée (Optional) * @return Entité trouvée (Optional)
*/ */
public Optional<T> findById(ID id) { public Optional<T> findById(ID id) {
try { try {
log.debug("Recherche de l'entité {} avec l'id : {}", log.debug("Recherche de l'entité {} avec l'id : {}",
entityClass.getSimpleName(), id); entityClass.getSimpleName(), id);
return Optional.ofNullable(entityManager.find(entityClass, id)); return Optional.ofNullable(entityManager.find(entityClass, id));
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException("Erreur lors de la recherche de l'entité", e); throw new RepositoryException("Erreur lors de la recherche de l'entité", e);
} }
} }
/** /**
* Récupère toutes les entités. * Récupère toutes les entités.
* *
* @return Liste des entités * @return Liste des entités
*/ */
public List<T> findAll() { public List<T> findAll() {
try { try {
log.debug("Récupération de toutes les entités : {}", log.debug("Récupération de toutes les entités : {}",
entityClass.getSimpleName()); entityClass.getSimpleName());
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<T> cq = cb.createQuery(entityClass); CriteriaQuery<T> cq = cb.createQuery(entityClass);
cq.from(entityClass); cq.from(entityClass);
TypedQuery<T> query = entityManager.createQuery(cq); TypedQuery<T> query = entityManager.createQuery(cq);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException("Erreur lors de la récupération des entités", e); throw new RepositoryException("Erreur lors de la récupération des entités", e);
} }
} }
/** /**
* Supprime une entité. * Supprime une entité.
* *
* @param entity Entité à supprimer * @param entity Entité à supprimer
*/ */
@Transactional @Transactional
public void delete(T entity) { public void delete(T entity) {
try { try {
log.debug("Suppression de l'entité : {}", entity); log.debug("Suppression de l'entité : {}", entity);
if (!entityManager.contains(entity)) { if (!entityManager.contains(entity)) {
entity = entityManager.merge(entity); entity = entityManager.merge(entity);
} }
entityManager.remove(entity); entityManager.remove(entity);
entityManager.flush(); entityManager.flush();
log.info("Entité supprimée avec succès : {}", entity); log.info("Entité supprimée avec succès : {}", entity);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException("Erreur lors de la suppression de l'entité", e); throw new RepositoryException("Erreur lors de la suppression de l'entité", e);
} }
} }
/** /**
* Supprime une entité par son identifiant. * Supprime une entité par son identifiant.
* *
* @param id Identifiant de l'entité à supprimer * @param id Identifiant de l'entité à supprimer
*/ */
@Transactional @Transactional
public void deleteById(ID id) { public void deleteById(ID id) {
try { try {
log.debug("Suppression de l'entité {} avec l'id : {}", log.debug("Suppression de l'entité {} avec l'id : {}",
entityClass.getSimpleName(), id); entityClass.getSimpleName(), id);
findById(id).ifPresent(this::delete); findById(id).ifPresent(this::delete);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException("Erreur lors de la suppression de l'entité", e); throw new RepositoryException("Erreur lors de la suppression de l'entité", e);
} }
} }
/** /**
* Vérifie l'existence d'une entité par son identifiant. * Vérifie l'existence d'une entité par son identifiant.
* *
* @param id Identifiant à vérifier * @param id Identifiant à vérifier
* @return true si l'entité existe * @return true si l'entité existe
*/ */
public boolean existsById(ID id) { public boolean existsById(ID id) {
try { try {
log.debug("Vérification de l'existence de l'entité {} avec l'id : {}", log.debug("Vérification de l'existence de l'entité {} avec l'id : {}",
entityClass.getSimpleName(), id); entityClass.getSimpleName(), id);
return findById(id).isPresent(); return findById(id).isPresent();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la vérification de l'existence de l'entité", e); "Erreur lors de la vérification de l'existence de l'entité", e);
} }
} }
/** /**
* Compte le nombre total d'entités. * Compte le nombre total d'entités.
* *
* @return Nombre total d'entités * @return Nombre total d'entités
*/ */
public long count() { public long count() {
try { try {
log.debug("Comptage des entités : {}", entityClass.getSimpleName()); log.debug("Comptage des entités : {}", entityClass.getSimpleName());
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> cq = cb.createQuery(Long.class); CriteriaQuery<Long> cq = cb.createQuery(Long.class);
cq.select(cb.count(cq.from(entityClass))); cq.select(cb.count(cq.from(entityClass)));
return entityManager.createQuery(cq).getSingleResult(); return entityManager.createQuery(cq).getSingleResult();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors du comptage des entités", e); "Erreur lors du comptage des entités", e);
} }
} }
} }

View File

@@ -1,188 +1,188 @@
package dev.lions.repositories; package dev.lions.repositories;
import dev.lions.models.Contact; import dev.lions.models.Contact;
import dev.lions.models.ContactStatus; import dev.lions.models.ContactStatus;
import dev.lions.exceptions.RepositoryException; import dev.lions.exceptions.RepositoryException;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
* Repository gérant la persistance des contacts dans l'application. * Repository gérant la persistance des contacts dans l'application.
* Cette classe assure le stockage, la récupération et la gestion des contacts * Cette classe assure le stockage, la récupération et la gestion des contacts
* en implémentant des fonctionnalités spécifiques au traitement des demandes * en implémentant des fonctionnalités spécifiques au traitement des demandes
* de contact. * de contact.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class ContactRepository extends BaseRepository<Contact, Long> { public class ContactRepository extends BaseRepository<Contact, Long> {
@PersistenceContext @PersistenceContext
private EntityManager entityManager; private EntityManager entityManager;
/** /**
* Recherche les contacts par statut avec tri par date de soumission. * Recherche les contacts par statut avec tri par date de soumission.
* Cette méthode permet de filtrer les contacts selon leur état de traitement. * Cette méthode permet de filtrer les contacts selon leur état de traitement.
* *
* @param status Statut des contacts à rechercher * @param status Statut des contacts à rechercher
* @return Liste des contacts correspondant au statut * @return Liste des contacts correspondant au statut
*/ */
public List<Contact> findByStatus(@NotNull ContactStatus status) { public List<Contact> findByStatus(@NotNull ContactStatus status) {
log.debug("Recherche des contacts avec le statut : {}", status); log.debug("Recherche des contacts avec le statut : {}", status);
try { try {
TypedQuery<Contact> query = entityManager.createQuery( TypedQuery<Contact> query = entityManager.createQuery(
"SELECT c FROM Contact c " + "SELECT c FROM Contact c " +
"WHERE c.status = :status " + "WHERE c.status = :status " +
"ORDER BY c.submitDate DESC", "ORDER BY c.submitDate DESC",
Contact.class Contact.class
); );
query.setParameter("status", status); query.setParameter("status", status);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des contacts par statut", e); "Erreur lors de la recherche des contacts par statut", e);
} }
} }
/** /**
* Récupère les contacts non traités pour suivi. * Récupère les contacts non traités pour suivi.
* Cette méthode retourne les contacts qui nécessitent une attention, * Cette méthode retourne les contacts qui nécessitent une attention,
* soit nouveaux soit en cours de traitement. * soit nouveaux soit en cours de traitement.
* *
* @return Liste des contacts à traiter * @return Liste des contacts à traiter
*/ */
public List<Contact> findUnprocessedContacts() { public List<Contact> findUnprocessedContacts() {
log.debug("Recherche des contacts non traités"); log.debug("Recherche des contacts non traités");
try { try {
TypedQuery<Contact> query = entityManager.createQuery( TypedQuery<Contact> query = entityManager.createQuery(
"SELECT c FROM Contact c " + "SELECT c FROM Contact c " +
"WHERE c.status IN (:statuses) " + "WHERE c.status IN (:statuses) " +
"ORDER BY c.submitDate ASC", "ORDER BY c.submitDate ASC",
Contact.class Contact.class
); );
query.setParameter("statuses", query.setParameter("statuses",
List.of(ContactStatus.NEW, ContactStatus.IN_PROGRESS)); List.of(ContactStatus.NEW, ContactStatus.IN_PROGRESS));
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des contacts non traités", e); "Erreur lors de la recherche des contacts non traités", e);
} }
} }
/** /**
* Recherche les contacts soumis dans une période donnée. * Recherche les contacts soumis dans une période donnée.
* *
* @param startDate Date de début de la période * @param startDate Date de début de la période
* @param endDate Date de fin de la période * @param endDate Date de fin de la période
* @return Liste des contacts pour la période * @return Liste des contacts pour la période
*/ */
public List<Contact> findBySubmitDateBetween( public List<Contact> findBySubmitDateBetween(
@NotNull LocalDateTime startDate, @NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) { @NotNull LocalDateTime endDate) {
log.debug("Recherche des contacts entre {} et {}", startDate, endDate); log.debug("Recherche des contacts entre {} et {}", startDate, endDate);
try { try {
TypedQuery<Contact> query = entityManager.createQuery( TypedQuery<Contact> query = entityManager.createQuery(
"SELECT c FROM Contact c " + "SELECT c FROM Contact c " +
"WHERE c.submitDate BETWEEN :startDate AND :endDate " + "WHERE c.submitDate BETWEEN :startDate AND :endDate " +
"ORDER BY c.submitDate DESC", "ORDER BY c.submitDate DESC",
Contact.class Contact.class
); );
query.setParameter("startDate", startDate); query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate); query.setParameter("endDate", endDate);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des contacts par période", e); "Erreur lors de la recherche des contacts par période", e);
} }
} }
/** /**
* Met à jour le statut d'un contact. * Met à jour le statut d'un contact.
* *
* @param contactId Identifiant du contact * @param contactId Identifiant du contact
* @param newStatus Nouveau statut * @param newStatus Nouveau statut
* @param processDate Date de traitement * @param processDate Date de traitement
* @return Contact mis à jour * @return Contact mis à jour
*/ */
public Optional<Contact> updateStatus( public Optional<Contact> updateStatus(
@NotNull Long contactId, @NotNull Long contactId,
@NotNull ContactStatus newStatus, @NotNull ContactStatus newStatus,
LocalDateTime processDate) { LocalDateTime processDate) {
log.debug("Mise à jour du statut du contact {} vers {}", contactId, newStatus); log.debug("Mise à jour du statut du contact {} vers {}", contactId, newStatus);
try { try {
Contact contact = entityManager.find(Contact.class, contactId); Contact contact = entityManager.find(Contact.class, contactId);
if (contact == null) { if (contact == null) {
return Optional.empty(); return Optional.empty();
} }
contact.setStatus(newStatus); contact.setStatus(newStatus);
if (processDate != null) { if (processDate != null) {
contact.setProcessDate(processDate); contact.setProcessDate(processDate);
} }
Contact updatedContact = update(contact); Contact updatedContact = update(contact);
log.info("Statut du contact {} mis à jour vers {}", contactId, newStatus); log.info("Statut du contact {} mis à jour vers {}", contactId, newStatus);
return Optional.of(updatedContact); return Optional.of(updatedContact);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la mise à jour du statut du contact", e); "Erreur lors de la mise à jour du statut du contact", e);
} }
} }
/** /**
* Ajoute une note interne à un contact. * Ajoute une note interne à un contact.
* *
* @param contactId Identifiant du contact * @param contactId Identifiant du contact
* @param note Note à ajouter * @param note Note à ajouter
* @return Contact mis à jour * @return Contact mis à jour
*/ */
public Optional<Contact> addInternalNote( public Optional<Contact> addInternalNote(
@NotNull Long contactId, @NotNull Long contactId,
@NotNull String note) { @NotNull String note) {
log.debug("Ajout d'une note au contact {}", contactId); log.debug("Ajout d'une note au contact {}", contactId);
try { try {
Contact contact = entityManager.find(Contact.class, contactId); Contact contact = entityManager.find(Contact.class, contactId);
if (contact == null) { if (contact == null) {
return Optional.empty(); return Optional.empty();
} }
String currentNotes = contact.getInternalNotes(); String currentNotes = contact.getInternalNotes();
String updatedNotes = currentNotes == null ? note : String updatedNotes = currentNotes == null ? note :
currentNotes + "\n" + LocalDateTime.now() + ": " + note; currentNotes + "\n" + LocalDateTime.now() + ": " + note;
contact.setInternalNotes(updatedNotes); contact.setInternalNotes(updatedNotes);
Contact updatedContact = update(contact); Contact updatedContact = update(contact);
log.info("Note ajoutée au contact {}", contactId); log.info("Note ajoutée au contact {}", contactId);
return Optional.of(updatedContact); return Optional.of(updatedContact);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de l'ajout de la note au contact", e); "Erreur lors de l'ajout de la note au contact", e);
} }
} }
} }

View File

@@ -1,209 +1,209 @@
package dev.lions.repositories; package dev.lions.repositories;
import dev.lions.models.EmailTemplate; import dev.lions.models.EmailTemplate;
import dev.lions.exceptions.RepositoryException; import dev.lions.exceptions.RepositoryException;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.NoResultException; import jakarta.persistence.NoResultException;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
* Repository gérant la persistance des modèles d'emails de l'application. * Repository gérant la persistance des modèles d'emails de l'application.
* Cette classe assure le stockage, la récupération et la gestion des templates * Cette classe assure le stockage, la récupération et la gestion des templates
* d'emails avec support multilingue et versionnement. * d'emails avec support multilingue et versionnement.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class EmailTemplateRepository extends BaseRepository<EmailTemplate, Long> { public class EmailTemplateRepository extends BaseRepository<EmailTemplate, Long> {
@PersistenceContext @PersistenceContext
private EntityManager entityManager; private EntityManager entityManager;
/** /**
* Recherche un modèle d'email par son nom. * Recherche un modèle d'email par son nom.
* Cette méthode récupère la dernière version active du modèle. * Cette méthode récupère la dernière version active du modèle.
* *
* @param templateName Nom du modèle recherché * @param templateName Nom du modèle recherché
* @return Modèle trouvé (Optional) * @return Modèle trouvé (Optional)
*/ */
public Optional<EmailTemplate> findByName(@NotBlank String templateName) { public Optional<EmailTemplate> findByName(@NotBlank String templateName) {
log.debug("Recherche du modèle d'email : {}", templateName); log.debug("Recherche du modèle d'email : {}", templateName);
try { try {
TypedQuery<EmailTemplate> query = entityManager.createQuery( TypedQuery<EmailTemplate> query = entityManager.createQuery(
"SELECT t FROM EmailTemplate t " + "SELECT t FROM EmailTemplate t " +
"WHERE t.templateName = :name " + "WHERE t.templateName = :name " +
"AND t.active = true " + "AND t.active = true " +
"ORDER BY t.version DESC", "ORDER BY t.version DESC",
EmailTemplate.class EmailTemplate.class
); );
query.setParameter("name", templateName); query.setParameter("name", templateName);
query.setMaxResults(1); query.setMaxResults(1);
return Optional.of(query.getSingleResult()); return Optional.of(query.getSingleResult());
} catch (NoResultException e) { } catch (NoResultException e) {
log.debug("Aucun modèle trouvé pour le nom : {}", templateName); log.debug("Aucun modèle trouvé pour le nom : {}", templateName);
return Optional.empty(); return Optional.empty();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche du modèle d'email", e); "Erreur lors de la recherche du modèle d'email", e);
} }
} }
/** /**
* Recherche un modèle d'email par son nom et sa locale. * Recherche un modèle d'email par son nom et sa locale.
* Permet de récupérer des modèles localisés spécifiques. * Permet de récupérer des modèles localisés spécifiques.
* *
* @param templateName Nom du modèle * @param templateName Nom du modèle
* @param locale Code de la langue * @param locale Code de la langue
* @return Modèle trouvé (Optional) * @return Modèle trouvé (Optional)
*/ */
public Optional<EmailTemplate> findByNameAndLocale( public Optional<EmailTemplate> findByNameAndLocale(
@NotBlank String templateName, @NotBlank String templateName,
@NotBlank String locale) { @NotBlank String locale) {
log.debug("Recherche du modèle d'email : {} pour la locale : {}", log.debug("Recherche du modèle d'email : {} pour la locale : {}",
templateName, locale); templateName, locale);
try { try {
TypedQuery<EmailTemplate> query = entityManager.createQuery( TypedQuery<EmailTemplate> query = entityManager.createQuery(
"SELECT t FROM EmailTemplate t " + "SELECT t FROM EmailTemplate t " +
"WHERE t.templateName = :name " + "WHERE t.templateName = :name " +
"AND t.locale = :locale " + "AND t.locale = :locale " +
"AND t.active = true " + "AND t.active = true " +
"ORDER BY t.version DESC", "ORDER BY t.version DESC",
EmailTemplate.class EmailTemplate.class
); );
query.setParameter("name", templateName); query.setParameter("name", templateName);
query.setParameter("locale", locale); query.setParameter("locale", locale);
query.setMaxResults(1); query.setMaxResults(1);
return Optional.of(query.getSingleResult()); return Optional.of(query.getSingleResult());
} catch (NoResultException e) { } catch (NoResultException e) {
log.debug("Aucun modèle trouvé pour le nom : {} et la locale : {}", log.debug("Aucun modèle trouvé pour le nom : {} et la locale : {}",
templateName, locale); templateName, locale);
return Optional.empty(); return Optional.empty();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche du modèle d'email localisé", e); "Erreur lors de la recherche du modèle d'email localisé", e);
} }
} }
/** /**
* Crée ou met à jour un modèle d'email. * Crée ou met à jour un modèle d'email.
* Gère automatiquement le versionnement des modèles. * Gère automatiquement le versionnement des modèles.
* *
* @param template Modèle à sauvegarder * @param template Modèle à sauvegarder
* @return Modèle sauvegardé * @return Modèle sauvegardé
*/ */
@Transactional @Transactional
@Override @Override
public EmailTemplate save(EmailTemplate template) { public EmailTemplate save(EmailTemplate template) {
log.debug("Sauvegarde du modèle d'email : {}", template.getTemplateName()); log.debug("Sauvegarde du modèle d'email : {}", template.getTemplateName());
try { try {
if (template.getId() == null) { if (template.getId() == null) {
setNextVersion(template); setNextVersion(template);
entityManager.persist(template); entityManager.persist(template);
} else { } else {
template = entityManager.merge(template); template = entityManager.merge(template);
} }
entityManager.flush(); entityManager.flush();
log.info("Modèle d'email sauvegardé avec succès : {}", log.info("Modèle d'email sauvegardé avec succès : {}",
template.getTemplateName()); template.getTemplateName());
return template; return template;
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la sauvegarde du modèle d'email", e); "Erreur lors de la sauvegarde du modèle d'email", e);
} }
} }
/** /**
* Définit la prochaine version pour un nouveau modèle. * Définit la prochaine version pour un nouveau modèle.
*/ */
private void setNextVersion(EmailTemplate template) { private void setNextVersion(EmailTemplate template) {
try { try {
TypedQuery<Long> query = entityManager.createQuery( TypedQuery<Long> query = entityManager.createQuery(
"SELECT MAX(t.version) FROM EmailTemplate t " + "SELECT MAX(t.version) FROM EmailTemplate t " +
"WHERE t.templateName = :name", "WHERE t.templateName = :name",
Long.class Long.class
); );
query.setParameter("name", template.getTemplateName()); query.setParameter("name", template.getTemplateName());
Long maxVersion = query.getSingleResult(); Long maxVersion = query.getSingleResult();
template.setVersion(maxVersion == null ? 1L : maxVersion + 1); template.setVersion(maxVersion == null ? 1L : maxVersion + 1);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la définition de la version du modèle", e); "Erreur lors de la définition de la version du modèle", e);
} }
} }
/** /**
* Supprime tous les modèles d'un certain nom. * Supprime tous les modèles d'un certain nom.
* *
* @param templateName Nom des modèles à supprimer * @param templateName Nom des modèles à supprimer
*/ */
@Transactional @Transactional
public void deleteByName(@NotBlank String templateName) { public void deleteByName(@NotBlank String templateName) {
log.debug("Suppression des modèles d'email : {}", templateName); log.debug("Suppression des modèles d'email : {}", templateName);
try { try {
int deletedCount = entityManager.createQuery( int deletedCount = entityManager.createQuery(
"DELETE FROM EmailTemplate t WHERE t.templateName = :name") "DELETE FROM EmailTemplate t WHERE t.templateName = :name")
.setParameter("name", templateName) .setParameter("name", templateName)
.executeUpdate(); .executeUpdate();
log.info("{} modèles d'email supprimés pour le nom : {}", log.info("{} modèles d'email supprimés pour le nom : {}",
deletedCount, templateName); deletedCount, templateName);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la suppression des modèles d'email", e); "Erreur lors de la suppression des modèles d'email", e);
} }
} }
/** /**
* Vérifie l'existence d'un modèle par son nom. * Vérifie l'existence d'un modèle par son nom.
* *
* @param templateName Nom du modèle à vérifier * @param templateName Nom du modèle à vérifier
* @return true si le modèle existe * @return true si le modèle existe
*/ */
public boolean existsByName(@NotBlank String templateName) { public boolean existsByName(@NotBlank String templateName) {
log.debug("Vérification de l'existence du modèle : {}", templateName); log.debug("Vérification de l'existence du modèle : {}", templateName);
try { try {
Long count = entityManager.createQuery( Long count = entityManager.createQuery(
"SELECT COUNT(t) FROM EmailTemplate t WHERE t.templateName = :name", "SELECT COUNT(t) FROM EmailTemplate t WHERE t.templateName = :name",
Long.class) Long.class)
.setParameter("name", templateName) .setParameter("name", templateName)
.getSingleResult(); .getSingleResult();
return count > 0; return count > 0;
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la vérification de l'existence du modèle", e); "Erreur lors de la vérification de l'existence du modèle", e);
} }
} }
} }

View File

@@ -1,238 +1,238 @@
package dev.lions.repositories; package dev.lions.repositories;
import dev.lions.models.Notification; import dev.lions.models.Notification;
import dev.lions.models.NotificationStatus; import dev.lions.models.NotificationStatus;
import dev.lions.models.NotificationType; import dev.lions.models.NotificationType;
import dev.lions.exceptions.RepositoryException; import dev.lions.exceptions.RepositoryException;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.NoResultException; import jakarta.persistence.NoResultException;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
/** /**
* Repository gérant la persistance des notifications système. * Repository gérant la persistance des notifications système.
* Cette classe assure le stockage et la récupération des notifications * Cette classe assure le stockage et la récupération des notifications
* avec support pour le filtrage par statut, type et période. * avec support pour le filtrage par statut, type et période.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class NotificationRepository extends BaseRepository<Notification, Long> { public class NotificationRepository extends BaseRepository<Notification, Long> {
@PersistenceContext @PersistenceContext
private EntityManager entityManager; private EntityManager entityManager;
/** /**
* Récupère les notifications non lues. * Récupère les notifications non lues.
* Cette méthode retourne les notifications qui nécessitent * Cette méthode retourne les notifications qui nécessitent
* l'attention des utilisateurs. * l'attention des utilisateurs.
* *
* @return Liste des notifications non lues * @return Liste des notifications non lues
*/ */
public List<Notification> findUnreadNotifications() { public List<Notification> findUnreadNotifications() {
log.debug("Recherche des notifications non lues"); log.debug("Recherche des notifications non lues");
try { try {
TypedQuery<Notification> query = entityManager.createQuery( TypedQuery<Notification> query = entityManager.createQuery(
"SELECT n FROM Notification n " + "SELECT n FROM Notification n " +
"WHERE n.status = :status " + "WHERE n.status = :status " +
"ORDER BY n.timestamp DESC", "ORDER BY n.timestamp DESC",
Notification.class Notification.class
); );
query.setParameter("status", NotificationStatus.UNREAD); query.setParameter("status", NotificationStatus.UNREAD);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des notifications non lues", e); "Erreur lors de la recherche des notifications non lues", e);
} }
} }
/** /**
* Recherche les notifications pour une période donnée. * Recherche les notifications pour une période donnée.
* *
* @param start Date de début * @param start Date de début
* @param end Date de fin * @param end Date de fin
* @return Liste des notifications pour la période * @return Liste des notifications pour la période
*/ */
public List<Notification> findNotificationsByDateRange( public List<Notification> findNotificationsByDateRange(
@NotNull LocalDateTime start, @NotNull LocalDateTime start,
@NotNull LocalDateTime end) { @NotNull LocalDateTime end) {
log.debug("Recherche des notifications entre {} et {}", start, end); log.debug("Recherche des notifications entre {} et {}", start, end);
try { try {
TypedQuery<Notification> query = entityManager.createQuery( TypedQuery<Notification> query = entityManager.createQuery(
"SELECT n FROM Notification n " + "SELECT n FROM Notification n " +
"WHERE n.timestamp BETWEEN :start AND :end " + "WHERE n.timestamp BETWEEN :start AND :end " +
"ORDER BY n.timestamp DESC", "ORDER BY n.timestamp DESC",
Notification.class Notification.class
); );
query.setParameter("start", start); query.setParameter("start", start);
query.setParameter("end", end); query.setParameter("end", end);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des notifications par période", e); "Erreur lors de la recherche des notifications par période", e);
} }
} }
/** /**
* Récupère les notifications critiques non lues. * Récupère les notifications critiques non lues.
* Ces notifications représentent des alertes importantes nécessitant * Ces notifications représentent des alertes importantes nécessitant
* une attention immédiate. * une attention immédiate.
* *
* @return Liste des notifications critiques * @return Liste des notifications critiques
*/ */
public List<Notification> findCriticalNotifications() { public List<Notification> findCriticalNotifications() {
log.debug("Recherche des notifications critiques"); log.debug("Recherche des notifications critiques");
try { try {
TypedQuery<Notification> query = entityManager.createQuery( TypedQuery<Notification> query = entityManager.createQuery(
"SELECT n FROM Notification n " + "SELECT n FROM Notification n " +
"WHERE n.type.isCritical = true " + "WHERE n.type.isCritical = true " +
"AND n.status = :status " + "AND n.status = :status " +
"ORDER BY n.timestamp DESC", "ORDER BY n.timestamp DESC",
Notification.class Notification.class
); );
query.setParameter("status", NotificationStatus.UNREAD); query.setParameter("status", NotificationStatus.UNREAD);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des notifications critiques", e); "Erreur lors de la recherche des notifications critiques", e);
} }
} }
/** /**
* Marque une notification comme lue. * Marque une notification comme lue.
* *
* @param notificationId Identifiant de la notification * @param notificationId Identifiant de la notification
*/ */
@Transactional @Transactional
public void markAsRead(@NotNull Long notificationId) { public void markAsRead(@NotNull Long notificationId) {
log.debug("Marquage de la notification {} comme lue", notificationId); log.debug("Marquage de la notification {} comme lue", notificationId);
try { try {
Notification notification = findById(notificationId) Notification notification = findById(notificationId)
.orElseThrow(() -> new NoResultException("Notification non trouvée")); .orElseThrow(() -> new NoResultException("Notification non trouvée"));
notification.setStatus(NotificationStatus.READ); notification.setStatus(NotificationStatus.READ);
notification.setReadTimestamp(LocalDateTime.now()); notification.setReadTimestamp(LocalDateTime.now());
update(notification); update(notification);
log.info("Notification {} marquée comme lue", notificationId); log.info("Notification {} marquée comme lue", notificationId);
} catch (NoResultException e) { } catch (NoResultException e) {
log.warn("Tentative de marquage d'une notification inexistante : {}", log.warn("Tentative de marquage d'une notification inexistante : {}",
notificationId); notificationId);
throw new RepositoryException("Notification non trouvée", e); throw new RepositoryException("Notification non trouvée", e);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors du marquage de la notification comme lue", e); "Erreur lors du marquage de la notification comme lue", e);
} }
} }
/** /**
* Marque toutes les notifications non lues comme lues. * Marque toutes les notifications non lues comme lues.
*/ */
@Transactional @Transactional
public void markAllAsRead() { public void markAllAsRead() {
log.debug("Marquage de toutes les notifications comme lues"); log.debug("Marquage de toutes les notifications comme lues");
try { try {
int updatedCount = entityManager.createQuery( int updatedCount = entityManager.createQuery(
"UPDATE Notification n " + "UPDATE Notification n " +
"SET n.status = :newStatus, n.readTimestamp = :timestamp " + "SET n.status = :newStatus, n.readTimestamp = :timestamp " +
"WHERE n.status = :oldStatus" "WHERE n.status = :oldStatus"
) )
.setParameter("newStatus", NotificationStatus.READ) .setParameter("newStatus", NotificationStatus.READ)
.setParameter("timestamp", LocalDateTime.now()) .setParameter("timestamp", LocalDateTime.now())
.setParameter("oldStatus", NotificationStatus.UNREAD) .setParameter("oldStatus", NotificationStatus.UNREAD)
.executeUpdate(); .executeUpdate();
log.info("{} notifications marquées comme lues", updatedCount); log.info("{} notifications marquées comme lues", updatedCount);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors du marquage de toutes les notifications comme lues", e); "Erreur lors du marquage de toutes les notifications comme lues", e);
} }
} }
/** /**
* Compte les notifications par type pour une période donnée. * Compte les notifications par type pour une période donnée.
* *
* @param start Date de début * @param start Date de début
* @param end Date de fin * @param end Date de fin
* @return Nombre de notifications par type * @return Nombre de notifications par type
*/ */
public Map<NotificationType, Long> countByType( public Map<NotificationType, Long> countByType(
@NotNull LocalDateTime start, @NotNull LocalDateTime start,
@NotNull LocalDateTime end) { @NotNull LocalDateTime end) {
log.debug("Comptage des notifications par type entre {} et {}", start, end); log.debug("Comptage des notifications par type entre {} et {}", start, end);
try { try {
List<Object[]> results = entityManager.createQuery( List<Object[]> results = entityManager.createQuery(
"SELECT n.type, COUNT(n) FROM Notification n " + "SELECT n.type, COUNT(n) FROM Notification n " +
"WHERE n.timestamp BETWEEN :start AND :end " + "WHERE n.timestamp BETWEEN :start AND :end " +
"GROUP BY n.type", "GROUP BY n.type",
Object[].class Object[].class
) )
.setParameter("start", start) .setParameter("start", start)
.setParameter("end", end) .setParameter("end", end)
.getResultList(); .getResultList();
return results.stream() return results.stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
row -> (NotificationType) row[0], row -> (NotificationType) row[0],
row -> (Long) row[1] row -> (Long) row[1]
)); ));
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors du comptage des notifications par type", e); "Erreur lors du comptage des notifications par type", e);
} }
} }
/** /**
* Supprime les notifications antérieures à une date donnée. * Supprime les notifications antérieures à une date donnée.
* *
* @param retentionDate Date de conservation des notifications * @param retentionDate Date de conservation des notifications
* @return Nombre de notifications supprimées * @return Nombre de notifications supprimées
*/ */
@Transactional @Transactional
public int deleteEventsOlderThan(@NotNull LocalDateTime retentionDate) { public int deleteEventsOlderThan(@NotNull LocalDateTime retentionDate) {
log.info("Suppression des notifications antérieures à {}", retentionDate); log.info("Suppression des notifications antérieures à {}", retentionDate);
try { try {
int deletedCount = entityManager.createQuery( int deletedCount = entityManager.createQuery(
"DELETE FROM Notification e WHERE e.timestamp < :retentionDate") "DELETE FROM Notification e WHERE e.timestamp < :retentionDate")
.setParameter("retentionDate", retentionDate) .setParameter("retentionDate", retentionDate)
.executeUpdate(); .executeUpdate();
log.info("{} notifications supprimées", deletedCount); log.info("{} notifications supprimées", deletedCount);
return deletedCount; return deletedCount;
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la suppression des anciennes notifications", e); "Erreur lors de la suppression des anciennes notifications", e);
} }
} }
} }

View File

@@ -1,228 +1,228 @@
package dev.lions.repositories; package dev.lions.repositories;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery; import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import dev.lions.models.Project; import dev.lions.models.Project;
import dev.lions.exceptions.RepositoryException; import dev.lions.exceptions.RepositoryException;
import java.util.Map; import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* Repository gérant la persistance des projets dans l'application. * Repository gérant la persistance des projets dans l'application.
* Cette classe assure le stockage, la recherche et la gestion des projets * Cette classe assure le stockage, la recherche et la gestion des projets
* avec support pour le filtrage par tags, dates et statuts. * avec support pour le filtrage par tags, dates et statuts.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class ProjectRepository extends BaseRepository<Project, String> { public class ProjectRepository extends BaseRepository<Project, String> {
@PersistenceContext @PersistenceContext
private EntityManager entityManager; private EntityManager entityManager;
/** /**
* Recherche les projets par ensemble de tags. * Recherche les projets par ensemble de tags.
* Cette méthode permet de filtrer les projets qui contiennent au moins * Cette méthode permet de filtrer les projets qui contiennent au moins
* un des tags spécifiés. * un des tags spécifiés.
* *
* @param tags Liste des tags à rechercher * @param tags Liste des tags à rechercher
* @return Liste des projets correspondants * @return Liste des projets correspondants
*/ */
public List<Project> findByTags(@NotEmpty Set<String> tags) { public List<Project> findByTags(@NotEmpty Set<String> tags) {
log.debug("Recherche de projets par tags : {}", tags); log.debug("Recherche de projets par tags : {}", tags);
try { try {
TypedQuery<Project> query = entityManager.createQuery( TypedQuery<Project> query = entityManager.createQuery(
"SELECT DISTINCT p FROM Project p " + "SELECT DISTINCT p FROM Project p " +
"JOIN p.tags t " + "JOIN p.tags t " +
"WHERE t IN :tags", "WHERE t IN :tags",
Project.class Project.class
); );
query.setParameter("tags", tags); query.setParameter("tags", tags);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des projets par tags", e); "Erreur lors de la recherche des projets par tags", e);
} }
} }
/** /**
* Recherche les projets mis en avant. * Recherche les projets mis en avant.
* Permet de récupérer les projets marqués comme "featured" pour * Permet de récupérer les projets marqués comme "featured" pour
* l'affichage en page d'accueil. * l'affichage en page d'accueil.
* *
* @param featured État de mise en avant recherché * @param featured État de mise en avant recherché
* @return Liste des projets mis en avant * @return Liste des projets mis en avant
*/ */
public List<Project> findByFeatured(boolean featured) { public List<Project> findByFeatured(boolean featured) {
log.debug("Recherche des projets featured={}", featured); log.debug("Recherche des projets featured={}", featured);
try { try {
TypedQuery<Project> query = entityManager.createQuery( TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " + "SELECT p FROM Project p " +
"WHERE p.featured = :featured " + "WHERE p.featured = :featured " +
"ORDER BY p.completionDate DESC", "ORDER BY p.completionDate DESC",
Project.class Project.class
); );
query.setParameter("featured", featured); query.setParameter("featured", featured);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des projets mis en avant", e); "Erreur lors de la recherche des projets mis en avant", e);
} }
} }
/** /**
* Recherche les projets complétés dans une période donnée. * Recherche les projets complétés dans une période donnée.
* *
* @param startDate Date de début * @param startDate Date de début
* @param endDate Date de fin * @param endDate Date de fin
* @return Liste des projets pour la période * @return Liste des projets pour la période
*/ */
public List<Project> findByCompletionDateBetween( public List<Project> findByCompletionDateBetween(
@NotNull LocalDateTime startDate, @NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) { @NotNull LocalDateTime endDate) {
log.debug("Recherche des projets complétés entre {} et {}", log.debug("Recherche des projets complétés entre {} et {}",
startDate, endDate); startDate, endDate);
try { try {
TypedQuery<Project> query = entityManager.createQuery( TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " + "SELECT p FROM Project p " +
"WHERE p.completionDate BETWEEN :startDate AND :endDate " + "WHERE p.completionDate BETWEEN :startDate AND :endDate " +
"ORDER BY p.completionDate DESC", "ORDER BY p.completionDate DESC",
Project.class Project.class
); );
query.setParameter("startDate", startDate); query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate); query.setParameter("endDate", endDate);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des projets par période", e); "Erreur lors de la recherche des projets par période", e);
} }
} }
/** /**
* Récupère les projets les plus récents. * Récupère les projets les plus récents.
* *
* @param limit Nombre maximum de projets à retourner * @param limit Nombre maximum de projets à retourner
* @return Liste limitée des projets les plus récents * @return Liste limitée des projets les plus récents
*/ */
public List<Project> findRecentProjects(int limit) { public List<Project> findRecentProjects(int limit) {
log.debug("Recherche des {} projets les plus récents", limit); log.debug("Recherche des {} projets les plus récents", limit);
try { try {
TypedQuery<Project> query = entityManager.createQuery( TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " + "SELECT p FROM Project p " +
"ORDER BY p.completionDate DESC", "ORDER BY p.completionDate DESC",
Project.class Project.class
); );
query.setMaxResults(limit); query.setMaxResults(limit);
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des projets récents", e); "Erreur lors de la recherche des projets récents", e);
} }
} }
/** /**
* Recherche les projets par technologie utilisée. * Recherche les projets par technologie utilisée.
* *
* @param technology Technologie recherchée * @param technology Technologie recherchée
* @return Liste des projets utilisant cette technologie * @return Liste des projets utilisant cette technologie
*/ */
public List<Project> findByTechnology(@NotNull String technology) { public List<Project> findByTechnology(@NotNull String technology) {
log.debug("Recherche des projets utilisant la technologie : {}", log.debug("Recherche des projets utilisant la technologie : {}",
technology); technology);
try { try {
TypedQuery<Project> query = entityManager.createQuery( TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " + "SELECT p FROM Project p " +
"JOIN p.technologies t " + "JOIN p.technologies t " +
"WHERE LOWER(t) = LOWER(:technology)", "WHERE LOWER(t) = LOWER(:technology)",
Project.class Project.class
); );
query.setParameter("technology", technology.toLowerCase()); query.setParameter("technology", technology.toLowerCase());
return query.getResultList(); return query.getResultList();
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la recherche des projets par technologie", e); "Erreur lors de la recherche des projets par technologie", e);
} }
} }
/** /**
* Met à jour le statut "featured" d'un projet. * Met à jour le statut "featured" d'un projet.
* *
* @param projectId Identifiant du projet * @param projectId Identifiant du projet
* @param featured Nouveau statut featured * @param featured Nouveau statut featured
*/ */
@Transactional @Transactional
public void updateFeaturedStatus(@NotNull String projectId, boolean featured) { public void updateFeaturedStatus(@NotNull String projectId, boolean featured) {
log.debug("Mise à jour du statut featured={} pour le projet {}", log.debug("Mise à jour du statut featured={} pour le projet {}",
featured, projectId); featured, projectId);
try { try {
Project project = findById(projectId) Project project = findById(projectId)
.orElseThrow(() -> new RepositoryException("Projet non trouvé")); .orElseThrow(() -> new RepositoryException("Projet non trouvé"));
project.setFeatured(featured); project.setFeatured(featured);
update(project); update(project);
log.info("Statut featured mis à jour pour le projet {}", projectId); log.info("Statut featured mis à jour pour le projet {}", projectId);
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors de la mise à jour du statut featured", e); "Erreur lors de la mise à jour du statut featured", e);
} }
} }
/** /**
* Compte les projets par technologie. * Compte les projets par technologie.
* *
* @return Nombre de projets par technologie * @return Nombre de projets par technologie
*/ */
public Map<String, Long> countByTechnology() { public Map<String, Long> countByTechnology() {
log.debug("Comptage des projets par technologie"); log.debug("Comptage des projets par technologie");
try { try {
List<Object[]> results = entityManager.createQuery( List<Object[]> results = entityManager.createQuery(
"SELECT t, COUNT(p) FROM Project p " + "SELECT t, COUNT(p) FROM Project p " +
"JOIN p.technologies t " + "JOIN p.technologies t " +
"GROUP BY t", "GROUP BY t",
Object[].class Object[].class
).getResultList(); ).getResultList();
return results.stream() return results.stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
row -> (String) row[0], row -> (String) row[0],
row -> (Long) row[1] row -> (Long) row[1]
)); ));
} catch (Exception e) { } catch (Exception e) {
throw new RepositoryException( throw new RepositoryException(
"Erreur lors du comptage des projets par technologie", e); "Erreur lors du comptage des projets par technologie", e);
} }
} }
} }

View File

@@ -0,0 +1,256 @@
package dev.lions.roi;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.Map;
import java.util.HashMap;
/**
* Calculateur de ROI pour démontrer la valeur de la digitalisation
*/
@ApplicationScoped
public class ROICalculator {
/**
* Calcule le ROI basé sur les gains de productivité
*/
public ROIResult calculateROI(ROIInput input) {
ROIResult result = new ROIResult();
// Calculs des gains annuels
double productivityGains = calculateProductivityGains(input);
double errorReduction = calculateErrorReduction(input);
double timesSavings = calculateTimeSavings(input);
double complianceGains = calculateComplianceGains(input);
double totalAnnualGains = productivityGains + errorReduction + timesSavings + complianceGains;
// Coûts
double implementationCost = input.getInvestmentAmount();
double annualMaintenanceCost = implementationCost * 0.15; // 15% par an
// ROI sur 3 ans
double totalGains3Years = totalAnnualGains * 3;
double totalCosts3Years = implementationCost + (annualMaintenanceCost * 3);
double roi3Years = ((totalGains3Years - totalCosts3Years) / totalCosts3Years) * 100;
// Période de retour sur investissement
double paybackPeriod = implementationCost / totalAnnualGains;
// Remplissage du résultat
result.setAnnualProductivityGains(productivityGains);
result.setAnnualErrorReduction(errorReduction);
result.setAnnualTimeSavings(timesSavings);
result.setAnnualComplianceGains(complianceGains);
result.setTotalAnnualGains(totalAnnualGains);
result.setImplementationCost(implementationCost);
result.setAnnualMaintenanceCost(annualMaintenanceCost);
result.setRoi3Years(roi3Years);
result.setPaybackPeriodMonths(paybackPeriod * 12);
result.setNetPresentValue3Years(totalGains3Years - totalCosts3Years);
// Détails par catégorie
result.setGainsByCategory(Map.of(
"Productivité", productivityGains,
"Réduction erreurs", errorReduction,
"Gain de temps", timesSavings,
"Conformité", complianceGains
));
return result;
}
private double calculateProductivityGains(ROIInput input) {
// Gain de productivité basé sur l'automatisation
double baseProductivity = input.getEmployeeCount() * input.getAverageSalary() * 0.12; // 12% de gain
// Ajustement selon les modules
double multiplier = 1.0;
if (input.getSelectedModules().contains("CRM")) multiplier += 0.15;
if (input.getSelectedModules().contains("STOCK")) multiplier += 0.10;
if (input.getSelectedModules().contains("COMPTA")) multiplier += 0.08;
if (input.getSelectedModules().contains("RH")) multiplier += 0.05;
return baseProductivity * multiplier;
}
private double calculateErrorReduction(ROIInput input) {
// Réduction des erreurs et reprises
double currentErrorCost = input.getTurnover() * 0.02; // 2% du CA en erreurs
double reductionRate = 0.70; // 70% de réduction des erreurs
return currentErrorCost * reductionRate;
}
private double calculateTimeSavings(ROIInput input) {
// Gain de temps sur les tâches administratives
double adminTimeHours = input.getEmployeeCount() * 2 * 250; // 2h/jour/employé, 250 jours/an
double hourlyRate = input.getAverageSalary() / (8 * 250); // Taux horaire
double timeSavingRate = 0.40; // 40% de gain de temps
return adminTimeHours * hourlyRate * timeSavingRate;
}
private double calculateComplianceGains(ROIInput input) {
// Évitement des pénalités et amendes
double potentialFines = 500000; // 500K FCFA de pénalités potentielles par an
double complianceImprovement = 0.80; // 80% d'amélioration de la conformité
return potentialFines * complianceImprovement;
}
/**
* Génère des recommandations basées sur le ROI
*/
public String generateRecommendations(ROIResult result) {
StringBuilder recommendations = new StringBuilder();
if (result.getRoi3Years() > 200) {
recommendations.append("🚀 ROI EXCELLENT (>200%) : Investissement hautement recommandé !\n\n");
} else if (result.getRoi3Years() > 100) {
recommendations.append("✅ ROI TRÈS BON (>100%) : Investissement très rentable.\n\n");
} else if (result.getRoi3Years() > 50) {
recommendations.append("👍 ROI CORRECT (>50%) : Investissement rentable à moyen terme.\n\n");
} else {
recommendations.append("⚠️ ROI FAIBLE (<50%) : Revoir la configuration ou étaler l'investissement.\n\n");
}
recommendations.append("POINTS CLÉS :\n");
recommendations.append(String.format("• Retour sur investissement en %.1f mois\n", result.getPaybackPeriodMonths()));
recommendations.append(String.format("• Gains annuels : %,.0f FCFA\n", result.getTotalAnnualGains()));
recommendations.append(String.format("• Bénéfice net sur 3 ans : %,.0f FCFA\n", result.getNetPresentValue3Years()));
recommendations.append("\nPRIORITÉS D'IMPLÉMENTATION :\n");
// Recommandations par gain le plus élevé
result.getGainsByCategory().entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.forEach(entry -> {
recommendations.append(String.format("• %s : %,.0f FCFA/an\n",
entry.getKey(), entry.getValue()));
});
return recommendations.toString();
}
/**
* Calcule le ROI pour différents scénarios
*/
public Map<String, ROIResult> calculateScenarios(ROIInput baseInput) {
Map<String, ROIResult> scenarios = new HashMap<>();
// Scénario conservateur (gains -30%)
ROIInput conservative = baseInput.copy();
conservative.setAverageSalary(baseInput.getAverageSalary() * 0.7);
scenarios.put("Conservateur", calculateROI(conservative));
// Scénario réaliste (base)
scenarios.put("Réaliste", calculateROI(baseInput));
// Scénario optimiste (gains +50%)
ROIInput optimistic = baseInput.copy();
optimistic.setAverageSalary(baseInput.getAverageSalary() * 1.5);
scenarios.put("Optimiste", calculateROI(optimistic));
return scenarios;
}
}
/**
* Données d'entrée pour le calcul ROI
*/
class ROIInput {
private int employeeCount;
private double averageSalary; // Salaire moyen annuel
private double turnover; // CA annuel
private double investmentAmount; // Montant investissement
private java.util.List<String> selectedModules;
private String sector;
// Constructeurs
public ROIInput() {}
public ROIInput copy() {
ROIInput copy = new ROIInput();
copy.employeeCount = this.employeeCount;
copy.averageSalary = this.averageSalary;
copy.turnover = this.turnover;
copy.investmentAmount = this.investmentAmount;
copy.selectedModules = new java.util.ArrayList<>(this.selectedModules);
copy.sector = this.sector;
return copy;
}
// Getters et Setters
public int getEmployeeCount() { return employeeCount; }
public void setEmployeeCount(int employeeCount) { this.employeeCount = employeeCount; }
public double getAverageSalary() { return averageSalary; }
public void setAverageSalary(double averageSalary) { this.averageSalary = averageSalary; }
public double getTurnover() { return turnover; }
public void setTurnover(double turnover) { this.turnover = turnover; }
public double getInvestmentAmount() { return investmentAmount; }
public void setInvestmentAmount(double investmentAmount) { this.investmentAmount = investmentAmount; }
public java.util.List<String> getSelectedModules() { return selectedModules; }
public void setSelectedModules(java.util.List<String> selectedModules) { this.selectedModules = selectedModules; }
public String getSector() { return sector; }
public void setSector(String sector) { this.sector = sector; }
}
/**
* Résultat du calcul ROI
*/
class ROIResult {
private double annualProductivityGains;
private double annualErrorReduction;
private double annualTimeSavings;
private double annualComplianceGains;
private double totalAnnualGains;
private double implementationCost;
private double annualMaintenanceCost;
private double roi3Years;
private double paybackPeriodMonths;
private double netPresentValue3Years;
private Map<String, Double> gainsByCategory;
// Constructeurs
public ROIResult() {}
// Getters et Setters
public double getAnnualProductivityGains() { return annualProductivityGains; }
public void setAnnualProductivityGains(double annualProductivityGains) { this.annualProductivityGains = annualProductivityGains; }
public double getAnnualErrorReduction() { return annualErrorReduction; }
public void setAnnualErrorReduction(double annualErrorReduction) { this.annualErrorReduction = annualErrorReduction; }
public double getAnnualTimeSavings() { return annualTimeSavings; }
public void setAnnualTimeSavings(double annualTimeSavings) { this.annualTimeSavings = annualTimeSavings; }
public double getAnnualComplianceGains() { return annualComplianceGains; }
public void setAnnualComplianceGains(double annualComplianceGains) { this.annualComplianceGains = annualComplianceGains; }
public double getTotalAnnualGains() { return totalAnnualGains; }
public void setTotalAnnualGains(double totalAnnualGains) { this.totalAnnualGains = totalAnnualGains; }
public double getImplementationCost() { return implementationCost; }
public void setImplementationCost(double implementationCost) { this.implementationCost = implementationCost; }
public double getAnnualMaintenanceCost() { return annualMaintenanceCost; }
public void setAnnualMaintenanceCost(double annualMaintenanceCost) { this.annualMaintenanceCost = annualMaintenanceCost; }
public double getRoi3Years() { return roi3Years; }
public void setRoi3Years(double roi3Years) { this.roi3Years = roi3Years; }
public double getPaybackPeriodMonths() { return paybackPeriodMonths; }
public void setPaybackPeriodMonths(double paybackPeriodMonths) { this.paybackPeriodMonths = paybackPeriodMonths; }
public double getNetPresentValue3Years() { return netPresentValue3Years; }
public void setNetPresentValue3Years(double netPresentValue3Years) { this.netPresentValue3Years = netPresentValue3Years; }
public Map<String, Double> getGainsByCategory() { return gainsByCategory; }
public void setGainsByCategory(Map<String, Double> gainsByCategory) { this.gainsByCategory = gainsByCategory; }
}

View File

@@ -0,0 +1,170 @@
package dev.lions.roi;
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.List;
import java.util.Arrays;
/**
* API REST pour le calculateur ROI
*/
@Path("/api/roi")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ROIResource {
@Inject
ROICalculator roiCalculator;
/**
* Calcule le ROI pour une configuration donnée
*/
@POST
@Path("/calculate")
public Response calculateROI(ROIInput input) {
try {
// Validation des données
if (input.getEmployeeCount() <= 0 || input.getInvestmentAmount() <= 0) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "Données invalides"))
.build();
}
ROIResult result = roiCalculator.calculateROI(input);
String recommendations = roiCalculator.generateRecommendations(result);
Map<String, Object> response = Map.of(
"result", result,
"recommendations", recommendations
);
return Response.ok(response).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du calcul ROI"))
.build();
}
}
/**
* Calcule plusieurs scénarios ROI
*/
@POST
@Path("/scenarios")
public Response calculateScenarios(ROIInput input) {
try {
Map<String, ROIResult> scenarios = roiCalculator.calculateScenarios(input);
return Response.ok(scenarios).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du calcul des scénarios"))
.build();
}
}
/**
* Calcule le ROI rapide basé sur un audit
*/
@GET
@Path("/quick/{auditId}")
public Response quickROI(@PathParam("auditId") Long auditId) {
try {
// Récupération des données d'audit (simulation)
ROIInput input = new ROIInput();
input.setEmployeeCount(10); // Valeur par défaut
input.setAverageSalary(2400000); // 200K FCFA/mois
input.setTurnover(50000000); // 50M FCFA/an
input.setInvestmentAmount(500000); // 500K FCFA
input.setSelectedModules(Arrays.asList("CRM", "COMPTA"));
input.setSector("commerce");
ROIResult result = roiCalculator.calculateROI(input);
String recommendations = roiCalculator.generateRecommendations(result);
Map<String, Object> response = Map.of(
"result", result,
"recommendations", recommendations,
"input", input
);
return Response.ok(response).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors du calcul ROI rapide"))
.build();
}
}
/**
* Retourne les paramètres par défaut selon le secteur
*/
@GET
@Path("/defaults/{sector}")
public Response getDefaults(@PathParam("sector") String sector) {
ROIInput defaults = new ROIInput();
switch (sector.toLowerCase()) {
case "commerce":
defaults.setAverageSalary(2400000); // 200K/mois
defaults.setSelectedModules(Arrays.asList("CRM", "STOCK", "COMPTA"));
break;
case "services":
defaults.setAverageSalary(3000000); // 250K/mois
defaults.setSelectedModules(Arrays.asList("CRM", "RH", "COMPTA"));
break;
case "industrie":
defaults.setAverageSalary(3600000); // 300K/mois
defaults.setSelectedModules(Arrays.asList("STOCK", "COMPTA", "RH"));
break;
default:
defaults.setAverageSalary(2400000);
defaults.setSelectedModules(Arrays.asList("CRM", "COMPTA"));
}
defaults.setSector(sector);
return Response.ok(defaults).build();
}
/**
* Génère un rapport ROI détaillé
*/
@POST
@Path("/report")
public Response generateReport(ROIInput input) {
try {
ROIResult result = roiCalculator.calculateROI(input);
Map<String, ROIResult> scenarios = roiCalculator.calculateScenarios(input);
String recommendations = roiCalculator.generateRecommendations(result);
Map<String, Object> report = Map.of(
"input", input,
"baseResult", result,
"scenarios", scenarios,
"recommendations", recommendations,
"summary", generateSummary(result)
);
return Response.ok(report).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur lors de la génération du rapport"))
.build();
}
}
private Map<String, Object> generateSummary(ROIResult result) {
return Map.of(
"roiPercentage", Math.round(result.getRoi3Years()),
"paybackMonths", Math.round(result.getPaybackPeriodMonths()),
"annualSavings", Math.round(result.getTotalAnnualGains()),
"netBenefit", Math.round(result.getNetPresentValue3Years()),
"recommendation", result.getRoi3Years() > 100 ? "RECOMMANDÉ" :
result.getRoi3Years() > 50 ? "ACCEPTABLE" : "À REVOIR"
);
}
}

View File

@@ -1,94 +1,94 @@
package dev.lions.security; package dev.lions.security;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.servlet.Filter; import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig; import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse; import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.io.IOException; import java.io.IOException;
/** /**
* Filtre de sécurité pour l'application. * Filtre de sécurité pour l'application.
* Implémente la logique de sécurité pour toutes les requêtes entrantes. * Implémente la logique de sécurité pour toutes les requêtes entrantes.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.0 * @version 1.0
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
@WebFilter(urlPatterns = "/*") @WebFilter(urlPatterns = "/*")
public class SecurityFilter implements Filter { public class SecurityFilter implements Filter {
/** /**
* Initialise le filtre. * Initialise le filtre.
* Cette méthode est appelée par le conteneur lors du démarrage. * Cette méthode est appelée par le conteneur lors du démarrage.
* *
* @param filterConfig Configuration du filtre * @param filterConfig Configuration du filtre
*/ */
@Override @Override
public void init(FilterConfig filterConfig) { public void init(FilterConfig filterConfig) {
log.info("Initialisation du filtre de sécurité"); log.info("Initialisation du filtre de sécurité");
} }
/** /**
* Applique la logique de filtrage sur chaque requête. * Applique la logique de filtrage sur chaque requête.
* *
* @param request La requête entrante * @param request La requête entrante
* @param response La réponse * @param response La réponse
* @param chain La chaîne de filtres * @param chain La chaîne de filtres
* @throws IOException En cas d'erreur d'entrée/sortie * @throws IOException En cas d'erreur d'entrée/sortie
* @throws ServletException En cas d'erreur de servlet * @throws ServletException En cas d'erreur de servlet
*/ */
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException { throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestUri = httpRequest.getRequestURI(); String requestUri = httpRequest.getRequestURI();
log.debug("Traitement de la requête: {}", requestUri); log.debug("Traitement de la requête: {}", requestUri);
try { try {
// Vérification de sécurité de base // Vérification de sécurité de base
if (isSecurityCheckPassed(httpRequest)) { if (isSecurityCheckPassed(httpRequest)) {
chain.doFilter(request, response); chain.doFilter(request, response);
} else { } else {
log.warn("Accès refusé pour la requête: {}", requestUri); log.warn("Accès refusé pour la requête: {}", requestUri);
HttpServletResponse httpResponse = (HttpServletResponse) response; HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Accès refusé"); httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Accès refusé");
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors du traitement de la requête: {}", requestUri, e); log.error("Erreur lors du traitement de la requête: {}", requestUri, e);
throw e; throw e;
} }
} }
/** /**
* Effectue les vérifications de sécurité nécessaires. * Effectue les vérifications de sécurité nécessaires.
* *
* @param request La requête HTTP à vérifier * @param request La requête HTTP à vérifier
* @return true si la requête passe les vérifications de sécurité * @return true si la requête passe les vérifications de sécurité
*/ */
private boolean isSecurityCheckPassed(HttpServletRequest request) { private boolean isSecurityCheckPassed(HttpServletRequest request) {
// Implémentez ici votre logique de sécurité spécifique // Implémentez ici votre logique de sécurité spécifique
// Par exemple : vérification des tokens, authentification, autorisations... // Par exemple : vérification des tokens, authentification, autorisations...
log.trace("Vérification de sécurité pour: {}", request.getRequestURI()); log.trace("Vérification de sécurité pour: {}", request.getRequestURI());
return true; // À adapter selon vos besoins de sécurité return true; // À adapter selon vos besoins de sécurité
} }
/** /**
* Méthode appelée lors de la destruction du filtre. * Méthode appelée lors de la destruction du filtre.
*/ */
@Override @Override
public void destroy() { public void destroy() {
log.info("Destruction du filtre de sécurité"); log.info("Destruction du filtre de sécurité");
} }
} }

View File

@@ -1,70 +1,70 @@
package dev.lions.security; package dev.lions.security;
import io.quarkus.runtime.annotations.RegisterForReflection; import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig; import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse; import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.io.IOException; import java.io.IOException;
/** /**
* Filtre de sécurité pour ajouter des en-têtes HTTP de sécurité. * Filtre de sécurité pour ajouter des en-têtes HTTP de sécurité.
* Ce filtre ajoute automatiquement les en-têtes de sécurité recommandés à toutes les réponses. * Ce filtre ajoute automatiquement les en-têtes de sécurité recommandés à toutes les réponses.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.0 * @version 1.0
*/ */
@Slf4j @Slf4j
@WebFilter("/*") @WebFilter("/*")
@ApplicationScoped @ApplicationScoped
@RegisterForReflection @RegisterForReflection
public class SecurityHeadersFilter implements jakarta.servlet.Filter { public class SecurityHeadersFilter implements jakarta.servlet.Filter {
private static final String CSP_POLICY = private static final String CSP_POLICY =
"default-src 'self'; " + "default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; " +
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; " + "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; " +
"img-src 'self' data: https:; " + "img-src 'self' data: https:; " +
"font-src 'self' https://cdnjs.cloudflare.com; " + "font-src 'self' https://cdnjs.cloudflare.com; " +
"connect-src 'self'"; "connect-src 'self'";
@Override @Override
public void init(FilterConfig filterConfig) throws ServletException { public void init(FilterConfig filterConfig) throws ServletException {
log.info("Initialisation du filtre des en-têtes de sécurité"); log.info("Initialisation du filtre des en-têtes de sécurité");
} }
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException { throws IOException, ServletException {
log.debug("Application des en-têtes de sécurité"); log.debug("Application des en-têtes de sécurité");
HttpServletResponse httpResponse = (HttpServletResponse) response; HttpServletResponse httpResponse = (HttpServletResponse) response;
// En-têtes de sécurité standards // En-têtes de sécurité standards
httpResponse.setHeader("X-Content-Type-Options", "nosniff"); httpResponse.setHeader("X-Content-Type-Options", "nosniff");
httpResponse.setHeader("X-Frame-Options", "SAMEORIGIN"); httpResponse.setHeader("X-Frame-Options", "SAMEORIGIN");
httpResponse.setHeader("X-XSS-Protection", "1; mode=block"); httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
httpResponse.setHeader("Content-Security-Policy", CSP_POLICY); httpResponse.setHeader("Content-Security-Policy", CSP_POLICY);
httpResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); httpResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
httpResponse.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()"); httpResponse.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
// Ajout des en-têtes HSTS en production // Ajout des en-têtes HSTS en production
if ("production".equals(System.getProperty("quarkus.profile"))) { if ("production".equals(System.getProperty("quarkus.profile"))) {
httpResponse.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); httpResponse.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
} }
chain.doFilter(request, response); chain.doFilter(request, response);
} }
@Override @Override
public void destroy() { public void destroy() {
log.info("Destruction du filtre des en-têtes de sécurité"); log.info("Destruction du filtre des en-têtes de sécurité");
} }
} }

View File

@@ -1,214 +1,214 @@
package dev.lions.services; package dev.lions.services;
import dev.lions.events.AnalyticsEvent; import dev.lions.events.AnalyticsEvent;
import dev.lions.events.AnalyticsEventPublisher; import dev.lions.events.AnalyticsEventPublisher;
import dev.lions.exceptions.AnalyticsException; import dev.lions.exceptions.AnalyticsException;
import dev.lions.repositories.AnalyticsRepository; import dev.lions.repositories.AnalyticsRepository;
import dev.lions.utils.MetricsCollector; import dev.lions.utils.MetricsCollector;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* Service responsable du traitement et de l'enregistrement des événements analytiques. * Service responsable du traitement et de l'enregistrement des événements analytiques.
* Gère l'enrichissement des données, leur persistance et leur publication. * Gère l'enrichissement des données, leur persistance et leur publication.
* *
* @author Lions Dev Team * @author Lions Dev Team
* @version 1.1 * @version 1.1
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class AnalyticsService { public class AnalyticsService {
private static final int MAX_RETRY_ATTEMPTS = 3; private static final int MAX_RETRY_ATTEMPTS = 3;
private static final long RETRY_DELAY_MS = 1000; private static final long RETRY_DELAY_MS = 1000;
private static final int BATCH_SIZE = 100; private static final int BATCH_SIZE = 100;
@Inject @Inject
AnalyticsRepository analyticsRepository; AnalyticsRepository analyticsRepository;
@Inject @Inject
AnalyticsEventPublisher eventPublisher; AnalyticsEventPublisher eventPublisher;
@Inject @Inject
private MetricsCollector metricsCollector; private MetricsCollector metricsCollector;
/** /**
* Traite et enregistre un événement analytique. * Traite et enregistre un événement analytique.
* L'événement est enrichi avec des métadonnées contextuelles avant son traitement. * L'événement est enrichi avec des métadonnées contextuelles avant son traitement.
* *
* @param event L'événement analytique à traiter * @param event L'événement analytique à traiter
* @return L'événement traité et enrichi * @return L'événement traité et enrichi
* @throws AnalyticsException Si une erreur survient pendant le traitement * @throws AnalyticsException Si une erreur survient pendant le traitement
*/ */
@Transactional @Transactional
public AnalyticsEvent processEvent(@NotNull @Valid AnalyticsEvent event) { public AnalyticsEvent processEvent(@NotNull @Valid AnalyticsEvent event) {
log.debug("Début du traitement de l'événement analytique de type: {}", event.getEventType()); log.debug("Début du traitement de l'événement analytique de type: {}", event.getEventType());
try { try {
// Enrichissement et validation // Enrichissement et validation
AnalyticsEvent enrichedEvent = enrichEventData(event); AnalyticsEvent enrichedEvent = enrichEventData(event);
validateEvent(enrichedEvent); validateEvent(enrichedEvent);
// Persistance avec gestion des reprises // Persistance avec gestion des reprises
AnalyticsEvent savedEvent = persistEventWithRetry(enrichedEvent); AnalyticsEvent savedEvent = persistEventWithRetry(enrichedEvent);
// Publication asynchrone // Publication asynchrone
publishEventAsync(savedEvent); publishEventAsync(savedEvent);
// Collecte des métriques // Collecte des métriques
metricsCollector.incrementEventCounter(event.getEventType()); metricsCollector.incrementEventCounter(event.getEventType());
log.info("Événement analytique traité avec succès - ID: {}, Type: {}", log.info("Événement analytique traité avec succès - ID: {}, Type: {}",
savedEvent.getId(), savedEvent.getEventType()); savedEvent.getId(), savedEvent.getEventType());
return savedEvent; return savedEvent;
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors du traitement de l'événement analytique", e); log.error("Erreur lors du traitement de l'événement analytique", e);
throw new AnalyticsException("Impossible de traiter l'événement analytique", e); throw new AnalyticsException("Impossible de traiter l'événement analytique", e);
} }
} }
/** /**
* Traite un lot d'événements analytiques de manière optimisée. * Traite un lot d'événements analytiques de manière optimisée.
* *
* @param events Liste des événements à traiter * @param events Liste des événements à traiter
* @return Liste des événements traités * @return Liste des événements traités
*/ */
@Transactional @Transactional
public List<AnalyticsEvent> processBatchEvents(List<AnalyticsEvent> events) { public List<AnalyticsEvent> processBatchEvents(List<AnalyticsEvent> events) {
log.debug("Traitement par lot de {} événements analytiques", events.size()); log.debug("Traitement par lot de {} événements analytiques", events.size());
return events.stream() return events.stream()
.map(this::processEvent) .map(this::processEvent)
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
} }
/** /**
* Récupère les événements analytiques pour une période donnée. * Récupère les événements analytiques pour une période donnée.
* *
* @param startDate Date de début * @param startDate Date de début
* @param endDate Date de fin * @param endDate Date de fin
* @return Liste des événements pour la période * @return Liste des événements pour la période
*/ */
public List<dev.lions.events.AnalyticsEvent> getEventsByDateRange(LocalDateTime startDate, LocalDateTime endDate) { public List<dev.lions.events.AnalyticsEvent> getEventsByDateRange(LocalDateTime startDate, LocalDateTime endDate) {
log.debug("Recherche des événements entre {} et {}", startDate, endDate); log.debug("Recherche des événements entre {} et {}", startDate, endDate);
return analyticsRepository.findEventsByDateRange(startDate, endDate); return analyticsRepository.findEventsByDateRange(startDate, endDate);
} }
/** /**
* Enrichit l'événement avec des données contextuelles supplémentaires. * Enrichit l'événement avec des données contextuelles supplémentaires.
* *
* @param event L'événement à enrichir * @param event L'événement à enrichir
* @return L'événement enrichi * @return L'événement enrichi
*/ */
private AnalyticsEvent enrichEventData(AnalyticsEvent event) { private AnalyticsEvent enrichEventData(AnalyticsEvent event) {
log.trace("Enrichissement des données de l'événement: {}", event.getId()); log.trace("Enrichissement des données de l'événement: {}", event.getId());
Map<String, Object> contextData = Map.of( Map<String, Object> contextData = Map.of(
"processTimestamp", LocalDateTime.now(), "processTimestamp", LocalDateTime.now(),
"processingNode", System.getProperty("jboss.node.name"), "processingNode", System.getProperty("jboss.node.name"),
"applicationVersion", System.getProperty("app.version") "applicationVersion", System.getProperty("app.version")
); );
return event.withAdditionalProperties(contextData) return event.withAdditionalProperties(contextData)
.enrichWithMetadata(); .enrichWithMetadata();
} }
/** /**
* Valide l'intégrité et la cohérence d'un événement. * Valide l'intégrité et la cohérence d'un événement.
* *
* @param event L'événement à valider * @param event L'événement à valider
* @throws AnalyticsException Si l'événement est invalide * @throws AnalyticsException Si l'événement est invalide
*/ */
private void validateEvent(AnalyticsEvent event) { private void validateEvent(AnalyticsEvent event) {
log.trace("Validation de l'événement analytique"); log.trace("Validation de l'événement analytique");
if (!event.isValid()) { if (!event.isValid()) {
log.warn("Validation échouée pour l'événement: {}", event); log.warn("Validation échouée pour l'événement: {}", event);
throw new AnalyticsException("L'événement analytique est invalide"); throw new AnalyticsException("L'événement analytique est invalide");
} }
} }
/** /**
* Persiste un événement avec mécanisme de reprise en cas d'échec. * Persiste un événement avec mécanisme de reprise en cas d'échec.
* *
* @param event L'événement à persister * @param event L'événement à persister
* @return L'événement persisté * @return L'événement persisté
* @throws AnalyticsException Si la persistance échoue après les reprises * @throws AnalyticsException Si la persistance échoue après les reprises
*/ */
private AnalyticsEvent persistEventWithRetry(AnalyticsEvent event) { private AnalyticsEvent persistEventWithRetry(AnalyticsEvent event) {
Exception lastException = null; Exception lastException = null;
for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) { for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
try { try {
return analyticsRepository.save(event); return analyticsRepository.save(event);
} catch (Exception e) { } catch (Exception e) {
lastException = e; lastException = e;
log.warn("Échec de la persistance (tentative {}/{}): {}", log.warn("Échec de la persistance (tentative {}/{}): {}",
attempt, MAX_RETRY_ATTEMPTS, e.getMessage()); attempt, MAX_RETRY_ATTEMPTS, e.getMessage());
if (attempt < MAX_RETRY_ATTEMPTS) { if (attempt < MAX_RETRY_ATTEMPTS) {
try { try {
TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS * attempt); TimeUnit.MILLISECONDS.sleep(RETRY_DELAY_MS * attempt);
} catch (InterruptedException ie) { } catch (InterruptedException ie) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new AnalyticsException("Interruption pendant la reprise", ie); throw new AnalyticsException("Interruption pendant la reprise", ie);
} }
} }
} }
} }
throw new AnalyticsException("Échec de la persistance après " + MAX_RETRY_ATTEMPTS + throw new AnalyticsException("Échec de la persistance après " + MAX_RETRY_ATTEMPTS +
" tentatives", lastException); " tentatives", lastException);
} }
/** /**
* Publie un événement de manière asynchrone. * Publie un événement de manière asynchrone.
* *
* @param event L'événement à publier * @param event L'événement à publier
*/ */
private void publishEventAsync(AnalyticsEvent event) { private void publishEventAsync(AnalyticsEvent event) {
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
try { try {
eventPublisher.publish(event); eventPublisher.publish(event);
log.debug("Publication asynchrone réussie pour l'événement: {}", event.getId()); log.debug("Publication asynchrone réussie pour l'événement: {}", event.getId());
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de la publication asynchrone de l'événement: {}", log.error("Erreur lors de la publication asynchrone de l'événement: {}",
event.getId(), e); event.getId(), e);
} }
}); });
} }
/** /**
* Nettoie les anciens événements selon la politique de rétention. * Nettoie les anciens événements selon la politique de rétention.
* *
* @param retentionDate Date limite de conservation * @param retentionDate Date limite de conservation
* @return Nombre d'événements supprimés * @return Nombre d'événements supprimés
*/ */
@Transactional @Transactional
public int cleanupOldEvents(LocalDateTime retentionDate) { public int cleanupOldEvents(LocalDateTime retentionDate) {
log.info("Nettoyage des événements antérieurs à {}", retentionDate); log.info("Nettoyage des événements antérieurs à {}", retentionDate);
try { try {
int deletedCount = analyticsRepository.deleteEventsOlderThan(retentionDate); int deletedCount = analyticsRepository.deleteEventsOlderThan(retentionDate);
log.info("{} événements anciens ont été supprimés", deletedCount); log.info("{} événements anciens ont été supprimés", deletedCount);
return deletedCount; return deletedCount;
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors du nettoyage des anciens événements", e); log.error("Erreur lors du nettoyage des anciens événements", e);
throw new AnalyticsException("Échec du nettoyage des événements", e); throw new AnalyticsException("Échec du nettoyage des événements", e);
} }
} }
} }

View File

@@ -1,229 +1,229 @@
package dev.lions.services; package dev.lions.services;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event; import jakarta.enterprise.event.Event;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import dev.lions.models.Contact; import dev.lions.models.Contact;
import dev.lions.models.ContactForm; import dev.lions.models.ContactForm;
import dev.lions.models.ContactStatus; import dev.lions.models.ContactStatus;
import dev.lions.models.EmailTemplate; import dev.lions.models.EmailTemplate;
import dev.lions.repositories.ContactRepository; import dev.lions.repositories.ContactRepository;
import dev.lions.events.ContactSubmissionEvent; import dev.lions.events.ContactSubmissionEvent;
import dev.lions.exceptions.BusinessException; import dev.lions.exceptions.BusinessException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Map; import java.util.Map;
/** /**
* Service gérant la logique métier des contacts. * Service gérant la logique métier des contacts.
* Cette classe assure le traitement des demandes de contact, leur validation, * Cette classe assure le traitement des demandes de contact, leur validation,
* et la notification des parties concernées. * et la notification des parties concernées.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class ContactService { public class ContactService {
@Inject @Inject
private ContactRepository contactRepository; private ContactRepository contactRepository;
@Inject @Inject
private EmailService emailService; private EmailService emailService;
@Inject @Inject
private Event<ContactSubmissionEvent> contactEvent; private Event<ContactSubmissionEvent> contactEvent;
/** /**
* Traite un nouveau formulaire de contact. * Traite un nouveau formulaire de contact.
* Cette méthode valide les données, enregistre le contact et envoie * Cette méthode valide les données, enregistre le contact et envoie
* les notifications appropriées. * les notifications appropriées.
* *
* @param form Formulaire de contact à traiter * @param form Formulaire de contact à traiter
* @return Contact créé * @return Contact créé
*/ */
@Transactional @Transactional
public Contact processContactForm(@Valid @NotNull ContactForm form) { public Contact processContactForm(@Valid @NotNull ContactForm form) {
log.info("Traitement d'une nouvelle demande de contact"); log.info("Traitement d'une nouvelle demande de contact");
try { try {
validateContactForm(form); validateContactForm(form);
Contact contact = createContact(form); Contact contact = createContact(form);
sendConfirmationEmails(contact); sendConfirmationEmails(contact);
notifyContactSubmission(contact); notifyContactSubmission(contact);
log.info("Demande de contact traitée avec succès - ID: {}", log.info("Demande de contact traitée avec succès - ID: {}",
contact.getId()); contact.getId());
return contact; return contact;
} catch (BusinessException be) { } catch (BusinessException be) {
log.warn("Erreur de validation du formulaire de contact", be); log.warn("Erreur de validation du formulaire de contact", be);
throw be; throw be;
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors du traitement de la demande de contact", e); log.error("Erreur lors du traitement de la demande de contact", e);
throw new BusinessException( throw new BusinessException(
"Impossible de traiter la demande de contact", e); "Impossible de traiter la demande de contact", e);
} }
} }
/** /**
* Valide les données du formulaire de contact. * Valide les données du formulaire de contact.
*/ */
private void validateContactForm(ContactForm form) { private void validateContactForm(ContactForm form) {
if (form.getName() == null || form.getName().trim().length() < 2) { if (form.getName() == null || form.getName().trim().length() < 2) {
throw new BusinessException("Le nom doit contenir au moins 2 caractères"); throw new BusinessException("Le nom doit contenir au moins 2 caractères");
} }
if (!isValidEmail(form.getEmail())) { if (!isValidEmail(form.getEmail())) {
throw new BusinessException("L'adresse email n'est pas valide"); throw new BusinessException("L'adresse email n'est pas valide");
} }
if (form.getMessage() == null || if (form.getMessage() == null ||
form.getMessage().trim().length() < 10 || form.getMessage().trim().length() < 10 ||
form.getMessage().length() > 1000) { form.getMessage().length() > 1000) {
throw new BusinessException( throw new BusinessException(
"Le message doit contenir entre 10 et 1000 caractères"); "Le message doit contenir entre 10 et 1000 caractères");
} }
} }
/** /**
* Crée une nouvelle entité Contact à partir du formulaire. * Crée une nouvelle entité Contact à partir du formulaire.
*/ */
private Contact createContact(ContactForm form) { private Contact createContact(ContactForm form) {
Contact contact = new Contact( Contact contact = new Contact(
form.getName(), form.getName(),
form.getEmail(), form.getEmail(),
form.getSubject(), form.getSubject(),
form.getMessage() form.getMessage()
); );
contact.setStatus(ContactStatus.NEW); contact.setStatus(ContactStatus.NEW);
contact.setSubmitDate(LocalDateTime.now()); contact.setSubmitDate(LocalDateTime.now());
return contactRepository.save(contact); return contactRepository.save(contact);
} }
/** /**
* Envoie les emails de confirmation. * Envoie les emails de confirmation.
*/ */
private void sendConfirmationEmails(Contact contact) { private void sendConfirmationEmails(Contact contact) {
sendCustomerConfirmation(contact); sendCustomerConfirmation(contact);
sendAdminNotification(contact); sendAdminNotification(contact);
} }
/** /**
* Envoie l'email de confirmation au client. * Envoie l'email de confirmation au client.
*/ */
private void sendCustomerConfirmation(Contact contact) { private void sendCustomerConfirmation(Contact contact) {
EmailTemplate template = EmailTemplate.builder() EmailTemplate template = EmailTemplate.builder()
.templateName("contact-confirmation") .templateName("contact-confirmation")
.recipient(contact.getEmail()) .recipient(contact.getEmail())
.subject("Confirmation de votre message") .subject("Confirmation de votre message")
.parameters(Map.of( .parameters(Map.of(
"name", contact.getName(), "name", contact.getName(),
"subject", contact.getSubject(), "subject", contact.getSubject(),
"message", contact.getMessage(), "message", contact.getMessage(),
"contactId", contact.getId().toString() "contactId", contact.getId().toString()
)) ))
.build(); .build();
emailService.sendTemplatedEmail(template); emailService.sendTemplatedEmail(template);
} }
/** /**
* Envoie l'email de notification à l'administrateur. * Envoie l'email de notification à l'administrateur.
*/ */
private void sendAdminNotification(Contact contact) { private void sendAdminNotification(Contact contact) {
EmailTemplate template = EmailTemplate.builder() EmailTemplate template = EmailTemplate.builder()
.templateName("admin-contact-notification") .templateName("admin-contact-notification")
.recipient(emailService.config.getAdminEmailAddress()) .recipient(emailService.config.getAdminEmailAddress())
.subject("Nouvelle demande de contact") .subject("Nouvelle demande de contact")
.parameters(Map.of( .parameters(Map.of(
"name", contact.getName(), "name", contact.getName(),
"email", contact.getEmail(), "email", contact.getEmail(),
"subject", contact.getSubject(), "subject", contact.getSubject(),
"message", contact.getMessage(), "message", contact.getMessage(),
"timestamp", contact.getSubmitDate().toString(), "timestamp", contact.getSubmitDate().toString(),
"contactId", contact.getId().toString() "contactId", contact.getId().toString()
)) ))
.build(); .build();
emailService.sendTemplatedEmail(template); emailService.sendTemplatedEmail(template);
} }
/** /**
* Notifie le système de la soumission d'un nouveau contact. * Notifie le système de la soumission d'un nouveau contact.
*/ */
private void notifyContactSubmission(Contact contact) { private void notifyContactSubmission(Contact contact) {
contactEvent.fire(new ContactSubmissionEvent(contact)); contactEvent.fire(new ContactSubmissionEvent(contact));
} }
/** /**
* Vérifie si une adresse email est valide. * Vérifie si une adresse email est valide.
*/ */
private boolean isValidEmail(String email) { private boolean isValidEmail(String email) {
if (email == null || email.trim().isEmpty()) { if (email == null || email.trim().isEmpty()) {
return false; return false;
} }
String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$"; String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$";
return email.matches(emailRegex); return email.matches(emailRegex);
} }
/** /**
* Met à jour le statut d'un contact. * Met à jour le statut d'un contact.
* *
* @param contactId Identifiant du contact * @param contactId Identifiant du contact
* @param newStatus Nouveau statut * @param newStatus Nouveau statut
* @param note Note optionnelle sur la mise à jour * @param note Note optionnelle sur la mise à jour
*/ */
@Transactional @Transactional
public void updateContactStatus( public void updateContactStatus(
@NotNull Long contactId, @NotNull Long contactId,
@NotNull ContactStatus newStatus, @NotNull ContactStatus newStatus,
String note) { String note) {
log.info("Mise à jour du statut du contact {} vers {}", log.info("Mise à jour du statut du contact {} vers {}",
contactId, newStatus); contactId, newStatus);
try { try {
Contact contact = contactRepository.findById(contactId) Contact contact = contactRepository.findById(contactId)
.orElseThrow(() -> new BusinessException("Contact non trouvé")); .orElseThrow(() -> new BusinessException("Contact non trouvé"));
contact.setStatus(newStatus); contact.setStatus(newStatus);
contact.setProcessDate(LocalDateTime.now()); contact.setProcessDate(LocalDateTime.now());
if (note != null && !note.trim().isEmpty()) { if (note != null && !note.trim().isEmpty()) {
addInternalNote(contact, note); addInternalNote(contact, note);
} }
contactRepository.update(contact); contactRepository.update(contact);
log.info("Statut du contact mis à jour avec succès"); log.info("Statut du contact mis à jour avec succès");
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de la mise à jour du statut du contact", e); log.error("Erreur lors de la mise à jour du statut du contact", e);
throw new BusinessException( throw new BusinessException(
"Impossible de mettre à jour le statut du contact", e); "Impossible de mettre à jour le statut du contact", e);
} }
} }
/** /**
* Ajoute une note interne à un contact. * Ajoute une note interne à un contact.
*/ */
private void addInternalNote(Contact contact, String note) { private void addInternalNote(Contact contact, String note) {
String currentNotes = contact.getInternalNotes(); String currentNotes = contact.getInternalNotes();
String timestamp = LocalDateTime.now().toString(); String timestamp = LocalDateTime.now().toString();
String newNote = String.format("[%s] %s", timestamp, note); String newNote = String.format("[%s] %s", timestamp, note);
if (currentNotes == null || currentNotes.trim().isEmpty()) { if (currentNotes == null || currentNotes.trim().isEmpty()) {
contact.setInternalNotes(newNote); contact.setInternalNotes(newNote);
} else { } else {
contact.setInternalNotes(currentNotes + "\n" + newNote); contact.setInternalNotes(currentNotes + "\n" + newNote);
} }
} }
} }

View File

@@ -1,201 +1,201 @@
package dev.lions.services; package dev.lions.services;
import dev.lions.models.EmailMessage; import dev.lions.models.EmailMessage;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.mail.Message; import jakarta.mail.Message;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import jakarta.mail.Session; import jakarta.mail.Session;
import jakarta.mail.Transport; import jakarta.mail.Transport;
import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import dev.lions.config.ApplicationConfig; import dev.lions.config.ApplicationConfig;
import dev.lions.models.EmailTemplate; import dev.lions.models.EmailTemplate;
import dev.lions.models.Notification; import dev.lions.models.Notification;
import dev.lions.exceptions.EmailException; import dev.lions.exceptions.EmailException;
import dev.lions.utils.TemplateProcessor; import dev.lions.utils.TemplateProcessor;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
/** /**
* Service gérant l'envoi des emails dans l'application. * Service gérant l'envoi des emails dans l'application.
* Cette classe assure la configuration SMTP, le traitement des modèles * Cette classe assure la configuration SMTP, le traitement des modèles
* et l'envoi sécurisé des emails. * et l'envoi sécurisé des emails.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class EmailService { public class EmailService {
@Inject @Inject
ApplicationConfig config; ApplicationConfig config;
@Inject @Inject
TemplateProcessor templateProcessor; TemplateProcessor templateProcessor;
private static final int MAX_RETRY_ATTEMPTS = 3; private static final int MAX_RETRY_ATTEMPTS = 3;
private static final long RETRY_DELAY_MS = 1000; private static final long RETRY_DELAY_MS = 1000;
/** /**
* Envoie un email basé sur un modèle. * Envoie un email basé sur un modèle.
* *
* @param template Modèle d'email à utiliser * @param template Modèle d'email à utiliser
*/ */
public void sendTemplatedEmail(@Valid @NotNull EmailTemplate template) { public void sendTemplatedEmail(@Valid @NotNull EmailTemplate template) {
log.info("Préparation de l'envoi d'email avec le modèle : {}", log.info("Préparation de l'envoi d'email avec le modèle : {}",
template.getTemplateName()); template.getTemplateName());
try { try {
String htmlContent = processTemplate(template); String htmlContent = processTemplate(template);
EmailMessage message = EmailMessage.builder() EmailMessage message = EmailMessage.builder()
.from(config.getSystemEmailAddress()) .from(config.getSystemEmailAddress())
.to(template.getRecipient()) .to(template.getRecipient())
.subject(template.getSubject()) .subject(template.getSubject())
.htmlContent(htmlContent) .htmlContent(htmlContent)
.build(); .build();
sendEmailWithRetry(message); sendEmailWithRetry(message);
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de l'envoi de l'email", e); log.error("Erreur lors de l'envoi de l'email", e);
throw new EmailException("Impossible d'envoyer l'email"); throw new EmailException("Impossible d'envoyer l'email");
} }
} }
/** /**
* Traite le contenu du modèle avec les paramètres fournis. * Traite le contenu du modèle avec les paramètres fournis.
*/ */
private String processTemplate(EmailTemplate template) { private String processTemplate(EmailTemplate template) {
log.debug("Traitement du modèle d'email : {}", template.getTemplateName()); log.debug("Traitement du modèle d'email : {}", template.getTemplateName());
return templateProcessor.process( return templateProcessor.process(
template.getContent(), template.getContent(),
template.getParameters() template.getParameters()
); );
} }
/** /**
* Envoie un email avec mécanisme de reprise en cas d'échec. * Envoie un email avec mécanisme de reprise en cas d'échec.
*/ */
private void sendEmailWithRetry(EmailMessage message) { private void sendEmailWithRetry(EmailMessage message) {
Exception lastException = null; Exception lastException = null;
for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) { for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
try { try {
sendEmail(message); sendEmail(message);
log.info("Email envoyé avec succès à : {}", message.getTo()); log.info("Email envoyé avec succès à : {}", message.getTo());
return; return;
} catch (Exception e) { } catch (Exception e) {
lastException = e; lastException = e;
log.warn("Échec de l'envoi (tentative {}/{})", log.warn("Échec de l'envoi (tentative {}/{})",
attempt, MAX_RETRY_ATTEMPTS); attempt, MAX_RETRY_ATTEMPTS);
if (attempt < MAX_RETRY_ATTEMPTS) { if (attempt < MAX_RETRY_ATTEMPTS) {
sleep(RETRY_DELAY_MS * attempt); sleep(RETRY_DELAY_MS * attempt);
} }
} }
} }
throw new EmailException( throw new EmailException(
"Échec de l'envoi après " + MAX_RETRY_ATTEMPTS + " tentatives"); "Échec de l'envoi après " + MAX_RETRY_ATTEMPTS + " tentatives");
} }
/** /**
* Envoie effectif de l'email via SMTP. * Envoie effectif de l'email via SMTP.
*/ */
private void sendEmail(EmailMessage message) throws MessagingException { private void sendEmail(EmailMessage message) throws MessagingException {
Properties props = configureSmtpProperties(); Properties props = configureSmtpProperties();
Session session = createSmtpSession(props); Session session = createSmtpSession(props);
MimeMessage mimeMessage = new MimeMessage(session); MimeMessage mimeMessage = new MimeMessage(session);
configureMimeMessage(mimeMessage, message); configureMimeMessage(mimeMessage, message);
Transport.send(mimeMessage); Transport.send(mimeMessage);
} }
/** /**
* Configure les propriétés SMTP. * Configure les propriétés SMTP.
*/ */
private Properties configureSmtpProperties() { private Properties configureSmtpProperties() {
Properties props = new Properties(); Properties props = new Properties();
props.put("mail.smtp.host", config.getSmtpHost()); props.put("mail.smtp.host", config.getSmtpHost());
props.put("mail.smtp.port", config.getSmtpPort()); props.put("mail.smtp.port", config.getSmtpPort());
props.put("mail.smtp.auth", "true"); props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true"); props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.connectiontimeout", "5000"); props.put("mail.smtp.connectiontimeout", "5000");
props.put("mail.smtp.timeout", "5000"); props.put("mail.smtp.timeout", "5000");
if (config.isSmtpSslEnabled()) { if (config.isSmtpSslEnabled()) {
props.put("mail.smtp.ssl.enable", "true"); props.put("mail.smtp.ssl.enable", "true");
props.put("mail.smtp.ssl.trust", config.getSmtpHost()); props.put("mail.smtp.ssl.trust", config.getSmtpHost());
} }
return props; return props;
} }
/** /**
* Crée une session SMTP authentifiée. * Crée une session SMTP authentifiée.
*/ */
private Session createSmtpSession(Properties props) { private Session createSmtpSession(Properties props) {
return Session.getInstance(props, new jakarta.mail.Authenticator() { return Session.getInstance(props, new jakarta.mail.Authenticator() {
@Override @Override
protected jakarta.mail.PasswordAuthentication getPasswordAuthentication() { protected jakarta.mail.PasswordAuthentication getPasswordAuthentication() {
return new jakarta.mail.PasswordAuthentication( return new jakarta.mail.PasswordAuthentication(
config.getSmtpUsername().orElseThrow(() -> config.getSmtpUsername().orElseThrow(() ->
new EmailException("Nom d'utilisateur SMTP manquant")), new EmailException("Nom d'utilisateur SMTP manquant")),
config.getSmtpPassword().orElseThrow(() -> config.getSmtpPassword().orElseThrow(() ->
new EmailException("Mot de passe SMTP manquant")) new EmailException("Mot de passe SMTP manquant"))
); );
} }
}); });
} }
/** /**
* Configure le message MIME avec les paramètres fournis. * Configure le message MIME avec les paramètres fournis.
*/ */
private void configureMimeMessage(MimeMessage mimeMessage, EmailMessage message) private void configureMimeMessage(MimeMessage mimeMessage, EmailMessage message)
throws MessagingException { throws MessagingException {
mimeMessage.setFrom(new InternetAddress(message.getFrom())); mimeMessage.setFrom(new InternetAddress(message.getFrom()));
mimeMessage.setRecipients( mimeMessage.setRecipients(
Message.RecipientType.TO, Message.RecipientType.TO,
InternetAddress.parse(message.getTo()) InternetAddress.parse(message.getTo())
); );
mimeMessage.setSubject(message.getSubject()); mimeMessage.setSubject(message.getSubject());
mimeMessage.setContent(message.getHtmlContent(), "text/html; charset=utf-8"); mimeMessage.setContent(message.getHtmlContent(), "text/html; charset=utf-8");
} }
/** /**
* Envoie une notification par email. * Envoie une notification par email.
*/ */
public void sendNotificationEmail(@NotNull Notification notification) { public void sendNotificationEmail(@NotNull Notification notification) {
EmailTemplate template = EmailTemplate.builder() EmailTemplate template = EmailTemplate.builder()
.templateName("notification-email") .templateName("notification-email")
.recipient(config.getAdminEmailAddress()) .recipient(config.getAdminEmailAddress())
.subject("Notification système : " + notification.getTitle()) .subject("Notification système : " + notification.getTitle())
.parameters(Map.of( .parameters(Map.of(
"title", notification.getTitle(), "title", notification.getTitle(),
"message", notification.getMessage(), "message", notification.getMessage(),
"type", notification.getType().toString(), "type", notification.getType().toString(),
"timestamp", notification.getTimestamp().toString(), "timestamp", notification.getTimestamp().toString(),
"actionUrl", notification.getActionUrl() "actionUrl", notification.getActionUrl()
)) ))
.build(); .build();
sendTemplatedEmail(template); sendTemplatedEmail(template);
} }
private void sleep(long milliseconds) { private void sleep(long milliseconds) {
try { try {
Thread.sleep(milliseconds); Thread.sleep(milliseconds);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new EmailException("Interruption pendant la reprise"); throw new EmailException("Interruption pendant la reprise");
} }
} }
} }

View File

@@ -1,33 +1,33 @@
package dev.lions.services; package dev.lions.services;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path; import java.nio.file.Path;
/** /**
* Service pour la gestion du stockage des fichiers. * Service pour la gestion du stockage des fichiers.
*/ */
@ApplicationScoped @ApplicationScoped
public interface FileStorageService { public interface FileStorageService {
void storeFile(String fileName); void storeFile(String fileName);
/** /**
* Stocke un fichier dans le répertoire spécifié. * Stocke un fichier dans le répertoire spécifié.
*/ */
Path storeFile(InputStream fileStream, String directory, String fileName); Path storeFile(InputStream fileStream, String directory, String fileName);
/** /**
* Crée un répertoire temporaire pour le stockage des fichiers. * Crée un répertoire temporaire pour le stockage des fichiers.
*/ */
String createTempDirectory(String prefix); String createTempDirectory(String prefix);
/** /**
* Supprime un fichier donné. * Supprime un fichier donné.
*/ */
void deleteFile(Path filePath); void deleteFile(Path filePath);
/** /**
* Supprime un répertoire et son contenu. * Supprime un répertoire et son contenu.
*/ */
void deleteDirectory(String directoryPath); void deleteDirectory(String directoryPath);
} }

View File

@@ -1,66 +1,66 @@
package dev.lions.services; package dev.lions.services;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path; import java.nio.file.Path;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
/** /**
* Implémentation du service de stockage de fichiers. * Implémentation du service de stockage de fichiers.
* Cette classe fournit des méthodes pour stocker des fichiers de manière basique. * Cette classe fournit des méthodes pour stocker des fichiers de manière basique.
* *
* <p>Elle est annotée avec {@link ApplicationScoped}, ce qui signifie que * <p>Elle est annotée avec {@link ApplicationScoped}, ce qui signifie que
* Quarkus gère son cycle de vie et garantit qu'une seule instance est créée * Quarkus gère son cycle de vie et garantit qu'une seule instance est créée
* pour toute l'application.</p> * pour toute l'application.</p>
*/ */
@ApplicationScoped @ApplicationScoped
public class FileStorageServiceImpl implements FileStorageService { public class FileStorageServiceImpl implements FileStorageService {
// Logger pour suivre les actions effectuées // Logger pour suivre les actions effectuées
private static final Logger LOG = Logger.getLogger(FileStorageServiceImpl.class); private static final Logger LOG = Logger.getLogger(FileStorageServiceImpl.class);
/** /**
* Méthode pour stocker un fichier. * Méthode pour stocker un fichier.
* *
* @param fileName Nom du fichier à stocker. * @param fileName Nom du fichier à stocker.
*/ */
@Override @Override
public void storeFile(String fileName) { public void storeFile(String fileName) {
// Log d'entrée pour la méthode // Log d'entrée pour la méthode
LOG.info("Début du stockage du fichier : " + fileName); LOG.info("Début du stockage du fichier : " + fileName);
try { try {
// Simulation d'un stockage de fichier // Simulation d'un stockage de fichier
System.out.println("Fichier stocké avec succès : " + fileName); System.out.println("Fichier stocké avec succès : " + fileName);
// Log de succès // Log de succès
LOG.info("Le fichier a été stocké avec succès."); LOG.info("Le fichier a été stocké avec succès.");
} catch (Exception e) { } catch (Exception e) {
// Gestion des erreurs avec log // Gestion des erreurs avec log
LOG.error("Erreur lors du stockage du fichier : " + fileName, e); LOG.error("Erreur lors du stockage du fichier : " + fileName, e);
} }
// Log de sortie pour la méthode // Log de sortie pour la méthode
LOG.debug("Fin du traitement de la méthode storeFile."); LOG.debug("Fin du traitement de la méthode storeFile.");
} }
@Override @Override
public Path storeFile(InputStream fileStream, String directory, String fileName) { public Path storeFile(InputStream fileStream, String directory, String fileName) {
return null; return null;
} }
@Override @Override
public String createTempDirectory(String prefix) { public String createTempDirectory(String prefix) {
return ""; return "";
} }
@Override @Override
public void deleteFile(Path filePath) { public void deleteFile(Path filePath) {
} }
@Override @Override
public void deleteDirectory(String directoryPath) { public void deleteDirectory(String directoryPath) {
} }
} }

View File

@@ -1,189 +1,189 @@
package dev.lions.services; package dev.lions.services;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.HashMap; import java.util.HashMap;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import dev.lions.models.Notification; import dev.lions.models.Notification;
import dev.lions.models.NotificationStatus; import dev.lions.models.NotificationStatus;
import dev.lions.models.NotificationType; import dev.lions.models.NotificationType;
import dev.lions.repositories.NotificationRepository; import dev.lions.repositories.NotificationRepository;
import dev.lions.dtos.NotificationDTO; import dev.lions.dtos.NotificationDTO;
import dev.lions.exceptions.NotificationException; import dev.lions.exceptions.NotificationException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
/** /**
* Service gérant les notifications système de l'application. * Service gérant les notifications système de l'application.
* Cette classe assure la création, l'envoi et le suivi des notifications * Cette classe assure la création, l'envoi et le suivi des notifications
* avec support pour différents types de notifications et canaux de distribution. * avec support pour différents types de notifications et canaux de distribution.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
public class NotificationService { public class NotificationService {
@Inject @Inject
NotificationRepository notificationRepository; NotificationRepository notificationRepository;
@Inject @Inject
WebSocketService webSocketService; WebSocketService webSocketService;
@Inject @Inject
EmailService emailService; EmailService emailService;
private static final int MAX_BATCH_SIZE = 100; private static final int MAX_BATCH_SIZE = 100;
/** /**
* Crée et envoie une nouvelle notification interne. * Crée et envoie une nouvelle notification interne.
* *
* @param type Type de notification * @param type Type de notification
* @param message Contenu de la notification * @param message Contenu de la notification
* @return Notification créée * @return Notification créée
*/ */
@Transactional @Transactional
public Notification sendInternalNotification( public Notification sendInternalNotification(
@NotNull NotificationType type, @NotNull NotificationType type,
@NotNull String message) { @NotNull String message) {
log.info("Création d'une notification interne de type : {}", type); log.info("Création d'une notification interne de type : {}", type);
try { try {
Notification notification = createNotification(type, message); Notification notification = createNotification(type, message);
notification = notificationRepository.save(notification); notification = notificationRepository.save(notification);
distributeNotification(notification); distributeNotification(notification);
log.info("Notification créée et distribuée avec succès - ID: {}", log.info("Notification créée et distribuée avec succès - ID: {}",
notification.getId()); notification.getId());
return notification; return notification;
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de l'envoi de la notification interne", e); log.error("Erreur lors de l'envoi de la notification interne", e);
throw new NotificationException( throw new NotificationException(
"Impossible d'envoyer la notification interne", e); "Impossible d'envoyer la notification interne", e);
} }
} }
/** /**
* Crée une nouvelle notification. * Crée une nouvelle notification.
*/ */
private Notification createNotification(NotificationType type, String message) { private Notification createNotification(NotificationType type, String message) {
return Notification.builder() return Notification.builder()
.title(generateTitle(type)) .title(generateTitle(type))
.message(message) .message(message)
.type(type) .type(type)
.status(NotificationStatus.UNREAD) .status(NotificationStatus.UNREAD)
.timestamp(LocalDateTime.now()) .timestamp(LocalDateTime.now())
.data(createNotificationData()) .data(createNotificationData())
.build(); .build();
} }
/** /**
* Distribue la notification via différents canaux. * Distribue la notification via différents canaux.
*/ */
private void distributeNotification(Notification notification) { private void distributeNotification(Notification notification) {
// Envoi WebSocket pour mise à jour en temps réel // Envoi WebSocket pour mise à jour en temps réel
webSocketService.broadcastToAdmins(NotificationDTO.from(notification)); webSocketService.broadcastToAdmins(NotificationDTO.from(notification));
// Envoi d'email pour les notifications critiques // Envoi d'email pour les notifications critiques
if (notification.isCritical()) { if (notification.isCritical()) {
emailService.sendNotificationEmail(notification); emailService.sendNotificationEmail(notification);
} }
} }
/** /**
* Génère un titre approprié selon le type de notification. * Génère un titre approprié selon le type de notification.
*/ */
private String generateTitle(NotificationType type) { private String generateTitle(NotificationType type) {
return switch (type) { return switch (type) {
case NEW_CONTACT -> "Nouveau message de contact"; case NEW_CONTACT -> "Nouveau message de contact";
case SYSTEM_ALERT -> "Alerte système"; case SYSTEM_ALERT -> "Alerte système";
case SECURITY_ALERT -> "Alerte de sécurité"; case SECURITY_ALERT -> "Alerte de sécurité";
default -> "Notification"; default -> "Notification";
}; };
} }
/** /**
* Crée les données additionnelles de la notification. * Crée les données additionnelles de la notification.
*/ */
private Notification.NotificationData createNotificationData() { private Notification.NotificationData createNotificationData() {
return Notification.NotificationData.builder() return Notification.NotificationData.builder()
.attributes(new HashMap<>()) .attributes(new HashMap<>())
.metadata(new HashMap<>()) .metadata(new HashMap<>())
.build(); .build();
} }
/** /**
* Récupère les notifications non lues. * Récupère les notifications non lues.
*/ */
public List<Notification> getUnreadNotifications() { public List<Notification> getUnreadNotifications() {
log.debug("Récupération des notifications non lues"); log.debug("Récupération des notifications non lues");
return notificationRepository.findUnreadNotifications(); return notificationRepository.findUnreadNotifications();
} }
/** /**
* Marque une notification comme lue. * Marque une notification comme lue.
*/ */
@Transactional @Transactional
public void markAsRead(@NotNull Long notificationId) { public void markAsRead(@NotNull Long notificationId) {
log.debug("Marquage de la notification {} comme lue", notificationId); log.debug("Marquage de la notification {} comme lue", notificationId);
notificationRepository.markAsRead(notificationId); notificationRepository.markAsRead(notificationId);
} }
/** /**
* Récupère les notifications critiques actives. * Récupère les notifications critiques actives.
*/ */
public List<Notification> getCriticalNotifications() { public List<Notification> getCriticalNotifications() {
log.debug("Récupération des notifications critiques"); log.debug("Récupération des notifications critiques");
return notificationRepository.findCriticalNotifications(); return notificationRepository.findCriticalNotifications();
} }
/** /**
* Nettoie les anciennes notifications. * Nettoie les anciennes notifications.
* *
* @param retentionDays Nombre de jours de rétention * @param retentionDays Nombre de jours de rétention
* @return Nombre de notifications supprimées * @return Nombre de notifications supprimées
*/ */
@Transactional @Transactional
public int cleanupOldNotifications(int retentionDays) { public int cleanupOldNotifications(int retentionDays) {
log.info("Nettoyage des notifications plus anciennes que {} jours", log.info("Nettoyage des notifications plus anciennes que {} jours",
retentionDays); retentionDays);
LocalDateTime retentionDate = LocalDateTime.now() LocalDateTime retentionDate = LocalDateTime.now()
.minusDays(retentionDays); .minusDays(retentionDays);
try { try {
int deletedCount = notificationRepository int deletedCount = notificationRepository
.deleteEventsOlderThan(retentionDate); .deleteEventsOlderThan(retentionDate);
log.info("{} notifications anciennes supprimées", deletedCount); log.info("{} notifications anciennes supprimées", deletedCount);
return deletedCount; return deletedCount;
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors du nettoyage des notifications", e); log.error("Erreur lors du nettoyage des notifications", e);
throw new NotificationException( throw new NotificationException(
"Impossible de nettoyer les anciennes notifications", e); "Impossible de nettoyer les anciennes notifications", e);
} }
} }
/** /**
* Récupère les statistiques des notifications. * Récupère les statistiques des notifications.
*/ */
public Map<NotificationType, Long> getNotificationStatistics( public Map<NotificationType, Long> getNotificationStatistics(
LocalDateTime startDate, LocalDateTime startDate,
LocalDateTime endDate) { LocalDateTime endDate) {
log.debug("Calcul des statistiques de notifications entre {} et {}", log.debug("Calcul des statistiques de notifications entre {} et {}",
startDate, endDate); startDate, endDate);
return notificationRepository.countByType(startDate, endDate); return notificationRepository.countByType(startDate, endDate);
} }
} }

View File

@@ -1,241 +1,241 @@
package dev.lions.services; package dev.lions.services;
import dev.lions.events.ProjectUpdateEvent; import dev.lions.events.ProjectUpdateEvent;
import dev.lions.exceptions.BusinessException; import dev.lions.exceptions.BusinessException;
import dev.lions.models.Project; import dev.lions.models.Project;
import dev.lions.models.Testimonial; import dev.lions.models.Testimonial;
import dev.lions.repositories.ProjectRepository; import dev.lions.repositories.ProjectRepository;
import dev.lions.utils.ImageProcessor; import dev.lions.utils.ImageProcessor;
import dev.lions.config.ApplicationConfig; import dev.lions.config.ApplicationConfig;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event; import jakarta.enterprise.event.Event;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.Set; import java.util.Set;
import lombok.Builder; import lombok.Builder;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* Service gérant la logique métier des projets. * Service gérant la logique métier des projets.
* Cette classe assure la gestion complète du cycle de vie des projets, * Cette classe assure la gestion complète du cycle de vie des projets,
* incluant leur création, mise à jour, recherche et validation. * incluant leur création, mise à jour, recherche et validation.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
@Builder @Builder
public class ProjectService { public class ProjectService {
@Inject @Inject
ProjectRepository projectRepository; ProjectRepository projectRepository;
@Inject @Inject
ImageProcessor imageProcessor; ImageProcessor imageProcessor;
@Inject @Inject
ApplicationConfig config; ApplicationConfig config;
@Inject @Inject
Event<ProjectUpdateEvent> projectUpdateEvent; Event<ProjectUpdateEvent> projectUpdateEvent;
/** /**
* Crée un nouveau projet avec son image associée. * Crée un nouveau projet avec son image associée.
* *
* @param project Données du projet * @param project Données du projet
* @param imageData Image du projet en bytes * @param imageData Image du projet en bytes
* @return Projet créé * @return Projet créé
*/ */
@Transactional @Transactional
public Project createProject(@Valid @NotNull Project project, byte[] imageData) { public Project createProject(@Valid @NotNull Project project, byte[] imageData) {
log.info("Création d'un nouveau projet : {}", project.getTitle()); log.info("Création d'un nouveau projet : {}", project.getTitle());
try { try {
validateProject(project); validateProject(project);
processProjectImage(project, imageData); processProjectImage(project, imageData);
Project savedProject = projectRepository.save(project); Project savedProject = projectRepository.save(project);
notifyProjectCreation(savedProject); notifyProjectCreation(savedProject);
log.info("Projet créé avec succès - ID: {}", savedProject.getId()); log.info("Projet créé avec succès - ID: {}", savedProject.getId());
return savedProject; return savedProject;
} catch (BusinessException be) { } catch (BusinessException be) {
log.warn("Validation échouée pour le projet", be); log.warn("Validation échouée pour le projet", be);
throw be; throw be;
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de la création du projet", e); log.error("Erreur lors de la création du projet", e);
throw new BusinessException("Impossible de créer le projet", e); throw new BusinessException("Impossible de créer le projet", e);
} }
} }
/** /**
* Met à jour un projet existant. * Met à jour un projet existant.
* *
* @param project Projet à mettre à jour * @param project Projet à mettre à jour
* @param imageData Nouvelle image optionnelle * @param imageData Nouvelle image optionnelle
* @return Projet mis à jour * @return Projet mis à jour
*/ */
@Transactional @Transactional
public Project updateProject(@Valid @NotNull Project project, byte[] imageData) { public Project updateProject(@Valid @NotNull Project project, byte[] imageData) {
log.info("Mise à jour du projet : {}", project.getId()); log.info("Mise à jour du projet : {}", project.getId());
try { try {
validateProject(project); validateProject(project);
if (imageData != null) { if (imageData != null) {
processProjectImage(project, imageData); processProjectImage(project, imageData);
} }
Project updatedProject = projectRepository.update(project); Project updatedProject = projectRepository.update(project);
notifyProjectUpdate(updatedProject); notifyProjectUpdate(updatedProject);
log.info("Projet mis à jour avec succès - ID: {}", updatedProject.getId()); log.info("Projet mis à jour avec succès - ID: {}", updatedProject.getId());
return updatedProject; return updatedProject;
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de la mise à jour du projet", e); log.error("Erreur lors de la mise à jour du projet", e);
throw new BusinessException("Impossible de mettre à jour le projet", e); throw new BusinessException("Impossible de mettre à jour le projet", e);
} }
} }
/** /**
* Traite et stocke l'image du projet. * Traite et stocke l'image du projet.
*/ */
private void processProjectImage(Project project, byte[] imageData) { private void processProjectImage(Project project, byte[] imageData) {
if (imageData == null || imageData.length == 0) { if (imageData == null || imageData.length == 0) {
throw new BusinessException("L'image du projet est requise"); throw new BusinessException("L'image du projet est requise");
} }
String imageUrl = imageProcessor.processAndStoreProjectImage( String imageUrl = imageProcessor.processAndStoreProjectImage(
imageData, imageData,
project.getTitle(), project.getTitle(),
config.getImageStoragePath() config.getImageStoragePath()
); );
project.setImageUrl(imageUrl); project.setImageUrl(imageUrl);
} }
/** /**
* Récupère un projet par son identifiant. * Récupère un projet par son identifiant.
*/ */
public Optional<Project> findById(@NotNull String projectId) { public Optional<Project> findById(@NotNull String projectId) {
log.debug("Recherche du projet : {}", projectId); log.debug("Recherche du projet : {}", projectId);
return projectRepository.findById(projectId); return projectRepository.findById(projectId);
} }
/** /**
* Vérifie l'existence d'un projet. * Vérifie l'existence d'un projet.
*/ */
public boolean existsById(@NotNull String projectId) { public boolean existsById(@NotNull String projectId) {
return projectRepository.existsById(projectId); return projectRepository.existsById(projectId);
} }
/** /**
* Récupère les projets filtrés par tag. * Récupère les projets filtrés par tag.
*/ */
public List<Project> getFilteredProjects(String filter) { public List<Project> getFilteredProjects(String filter) {
log.debug("Filtrage des projets avec le critère : {}", filter); log.debug("Filtrage des projets avec le critère : {}", filter);
if ("all".equalsIgnoreCase(filter)) { if ("all".equalsIgnoreCase(filter)) {
return projectRepository.findAll(); return projectRepository.findAll();
} }
return projectRepository.findByTags(Set.of(filter.toLowerCase())); return projectRepository.findByTags(Set.of(filter.toLowerCase()));
} }
/** /**
* Récupère les projets mis en avant. * Récupère les projets mis en avant.
*/ */
public List<Project> getFeaturedProjects() { public List<Project> getFeaturedProjects() {
log.debug("Récupération des projets mis en avant"); log.debug("Récupération des projets mis en avant");
return projectRepository.findByFeatured(true); return projectRepository.findByFeatured(true);
} }
/** /**
* Récupère les projets récents. * Récupère les projets récents.
*/ */
public List<Project> getRecentProjects(int limit) { public List<Project> getRecentProjects(int limit) {
log.debug("Récupération des {} projets les plus récents", limit); log.debug("Récupération des {} projets les plus récents", limit);
return projectRepository.findRecentProjects(limit); return projectRepository.findRecentProjects(limit);
} }
/** /**
* Récupère le nombre total de projets. * Récupère le nombre total de projets.
*/ */
public long getProjectCount() { public long getProjectCount() {
log.debug("Récupération du nombre total de projets"); log.debug("Récupération du nombre total de projets");
return projectRepository.count(); return projectRepository.count();
} }
/** /**
* Récupère les témoignages mis en avant. * Récupère les témoignages mis en avant.
*/ */
public List<Testimonial> getFeaturedTestimonials() { public List<Testimonial> getFeaturedTestimonials() {
log.debug("Récupération des témoignages mis en avant"); log.debug("Récupération des témoignages mis en avant");
return projectRepository.findByFeatured(true).stream() return projectRepository.findByFeatured(true).stream()
.filter(project -> !project.getTestimonials().isEmpty()) .filter(project -> !project.getTestimonials().isEmpty())
.map(this::createTestimonialFromProject) .map(this::createTestimonialFromProject)
.limit(3) .limit(3)
.toList(); .toList();
} }
/** /**
* Valide les données d'un projet. * Valide les données d'un projet.
*/ */
private void validateProject(Project project) { private void validateProject(Project project) {
if (project.getTitle() == null || project.getTitle().trim().isEmpty()) { if (project.getTitle() == null || project.getTitle().trim().isEmpty()) {
throw new BusinessException("Le titre du projet est requis"); throw new BusinessException("Le titre du projet est requis");
} }
if (project.getDescription() == null || project.getDescription().trim().isEmpty()) { if (project.getDescription() == null || project.getDescription().trim().isEmpty()) {
throw new BusinessException("La description du projet est requise"); throw new BusinessException("La description du projet est requise");
} }
if (project.getShortDescription() == null || if (project.getShortDescription() == null ||
project.getShortDescription().trim().isEmpty()) { project.getShortDescription().trim().isEmpty()) {
throw new BusinessException("La description courte du projet est requise"); throw new BusinessException("La description courte du projet est requise");
} }
} }
/** /**
* Crée un témoignage à partir d'un projet. * Crée un témoignage à partir d'un projet.
*/ */
private Testimonial createTestimonialFromProject(Project project) { private Testimonial createTestimonialFromProject(Project project) {
return Testimonial.builder() return Testimonial.builder()
.clientName(project.getClientName()) .clientName(project.getClientName())
.content(project.getTestimonials().get(0)) .content(project.getTestimonials().get(0))
.projectTitle(project.getTitle()) .projectTitle(project.getTitle())
.date(project.getCompletionDate()) .date(project.getCompletionDate())
.build(); .build();
} }
/** /**
* Notifie la création d'un projet. * Notifie la création d'un projet.
*/ */
private void notifyProjectCreation(Project project) { private void notifyProjectCreation(Project project) {
projectUpdateEvent.fire(new ProjectUpdateEvent( projectUpdateEvent.fire(new ProjectUpdateEvent(
project.getId(), project.getId(),
"CREATE", "CREATE",
LocalDateTime.now() LocalDateTime.now()
)); ));
} }
/** /**
* Notifie la mise à jour d'un projet. * Notifie la mise à jour d'un projet.
*/ */
private void notifyProjectUpdate(Project project) { private void notifyProjectUpdate(Project project) {
projectUpdateEvent.fire(new ProjectUpdateEvent( projectUpdateEvent.fire(new ProjectUpdateEvent(
project.getId(), project.getId(),
"UPDATE", "UPDATE",
LocalDateTime.now() LocalDateTime.now()
)); ));
} }
} }

View File

@@ -1,209 +1,209 @@
package dev.lions.services; package dev.lions.services;
import dev.lions.dtos.NotificationDTO; import dev.lions.dtos.NotificationDTO;
import dev.lions.exceptions.WebSocketException; import dev.lions.exceptions.WebSocketException;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.websocket.*; import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint; import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
* Service gérant les communications WebSocket de l'application. * Service gérant les communications WebSocket de l'application.
* Cette classe assure la gestion des connexions temps réel et la diffusion * Cette classe assure la gestion des connexions temps réel et la diffusion
* des notifications aux clients connectés. * des notifications aux clients connectés.
*/ */
@Slf4j @Slf4j
@ApplicationScoped @ApplicationScoped
@ServerEndpoint("/ws/notifications") @ServerEndpoint("/ws/notifications")
public class WebSocketService { public class WebSocketService {
private static final Map<String, Session> sessions = new ConcurrentHashMap<>(); private static final Map<String, Session> sessions = new ConcurrentHashMap<>();
private static final int MAX_MESSAGE_SIZE = 8192; private static final int MAX_MESSAGE_SIZE = 8192;
private static final long IDLE_TIMEOUT = 300000; // 5 minutes private static final long IDLE_TIMEOUT = 300000; // 5 minutes
/** /**
* Gère l'ouverture d'une nouvelle connexion WebSocket. * Gère l'ouverture d'une nouvelle connexion WebSocket.
* *
* @param session Session WebSocket ouverte * @param session Session WebSocket ouverte
*/ */
@OnOpen @OnOpen
public void onOpen(Session session) { public void onOpen(Session session) {
log.info("Nouvelle connexion WebSocket établie : {}", session.getId()); log.info("Nouvelle connexion WebSocket établie : {}", session.getId());
try { try {
configureSession(session); configureSession(session);
sessions.put(session.getId(), session); sessions.put(session.getId(), session);
sendWelcomeMessage(session); sendWelcomeMessage(session);
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors de l'initialisation de la session WebSocket", e); log.error("Erreur lors de l'initialisation de la session WebSocket", e);
closeSession(session, "Erreur d'initialisation"); closeSession(session, "Erreur d'initialisation");
} }
} }
/** /**
* Configure une nouvelle session WebSocket. * Configure une nouvelle session WebSocket.
*/ */
private void configureSession(Session session) { private void configureSession(Session session) {
session.setMaxIdleTimeout(IDLE_TIMEOUT); session.setMaxIdleTimeout(IDLE_TIMEOUT);
session.setMaxTextMessageBufferSize(MAX_MESSAGE_SIZE); session.setMaxTextMessageBufferSize(MAX_MESSAGE_SIZE);
session.setMaxBinaryMessageBufferSize(MAX_MESSAGE_SIZE); session.setMaxBinaryMessageBufferSize(MAX_MESSAGE_SIZE);
} }
/** /**
* Envoie un message de bienvenue au client connecté. * Envoie un message de bienvenue au client connecté.
*/ */
private void sendWelcomeMessage(Session session) { private void sendWelcomeMessage(Session session) {
try { try {
String message = "{\"type\":\"welcome\",\"message\":\"Connexion établie\"}"; String message = "{\"type\":\"welcome\",\"message\":\"Connexion établie\"}";
session.getBasicRemote().sendText(message); session.getBasicRemote().sendText(message);
} catch (Exception e) { } catch (Exception e) {
log.warn("Impossible d'envoyer le message de bienvenue", e); log.warn("Impossible d'envoyer le message de bienvenue", e);
} }
} }
/** /**
* Gère la réception d'un message depuis un client. * Gère la réception d'un message depuis un client.
* *
* @param message Message reçu * @param message Message reçu
* @param session Session WebSocket active * @param session Session WebSocket active
*/ */
@OnMessage @OnMessage
public void onMessage(String message, Session session) { public void onMessage(String message, Session session) {
String sessionId = session.getId(); String sessionId = session.getId();
log.debug("Message reçu de la session {} : {}", sessionId, message); log.debug("Message reçu de la session {} : {}", sessionId, message);
try { try {
validateMessage(message); validateMessage(message);
processMessage(message, session); processMessage(message, session);
} catch (Exception e) { } catch (Exception e) {
log.error("Erreur lors du traitement du message", e); log.error("Erreur lors du traitement du message", e);
sendErrorResponse(session, "Erreur de traitement du message"); sendErrorResponse(session, "Erreur de traitement du message");
} }
} }
/** /**
* Valide le contenu d'un message reçu. * Valide le contenu d'un message reçu.
*/ */
private void validateMessage(String message) { private void validateMessage(String message) {
if (message == null || message.trim().isEmpty()) { if (message == null || message.trim().isEmpty()) {
throw new WebSocketException("Message vide non autorisé"); throw new WebSocketException("Message vide non autorisé");
} }
if (message.length() > MAX_MESSAGE_SIZE) { if (message.length() > MAX_MESSAGE_SIZE) {
throw new WebSocketException("Message trop long"); throw new WebSocketException("Message trop long");
} }
} }
/** /**
* Traite un message reçu. * Traite un message reçu.
*/ */
private void processMessage(String message, Session session) { private void processMessage(String message, Session session) {
// Implémentation du traitement des messages selon les besoins // Implémentation du traitement des messages selon les besoins
log.debug("Traitement du message de la session {}", session.getId()); log.debug("Traitement du message de la session {}", session.getId());
} }
/** /**
* Gère la fermeture d'une connexion WebSocket. * Gère la fermeture d'une connexion WebSocket.
* *
* @param session Session WebSocket fermée * @param session Session WebSocket fermée
*/ */
@OnClose @OnClose
public void onClose(Session session) { public void onClose(Session session) {
String sessionId = session.getId(); String sessionId = session.getId();
log.info("Fermeture de la connexion WebSocket : {}", sessionId); log.info("Fermeture de la connexion WebSocket : {}", sessionId);
sessions.remove(sessionId); sessions.remove(sessionId);
cleanupSession(session); cleanupSession(session);
} }
/** /**
* Gère les erreurs survenant sur une connexion WebSocket. * Gère les erreurs survenant sur une connexion WebSocket.
* *
* @param session Session WebSocket concernée * @param session Session WebSocket concernée
* @param throwable Erreur survenue * @param throwable Erreur survenue
*/ */
@OnError @OnError
public void onError(Session session, Throwable throwable) { public void onError(Session session, Throwable throwable) {
String sessionId = session.getId(); String sessionId = session.getId();
log.error("Erreur WebSocket sur la session {} : {}", log.error("Erreur WebSocket sur la session {} : {}",
sessionId, throwable.getMessage(), throwable); sessionId, throwable.getMessage(), throwable);
closeSession(session, "Erreur interne"); closeSession(session, "Erreur interne");
} }
/** /**
* Diffuse une notification à tous les clients connectés. * Diffuse une notification à tous les clients connectés.
* *
* @param notification Notification à diffuser * @param notification Notification à diffuser
*/ */
public void broadcastToAdmins(NotificationDTO notification) { public void broadcastToAdmins(NotificationDTO notification) {
log.debug("Diffusion d'une notification à {} clients", sessions.size()); log.debug("Diffusion d'une notification à {} clients", sessions.size());
String message = notification.toJson(); String message = notification.toJson();
sessions.values().forEach(session -> { sessions.values().forEach(session -> {
try { try {
if (session.isOpen()) { if (session.isOpen()) {
session.getBasicRemote().sendText(message); session.getBasicRemote().sendText(message);
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("Erreur lors de l'envoi à la session {}", log.warn("Erreur lors de l'envoi à la session {}",
session.getId(), e); session.getId(), e);
} }
}); });
} }
/** /**
* Envoie une réponse d'erreur à un client. * Envoie une réponse d'erreur à un client.
*/ */
private void sendErrorResponse(Session session, String errorMessage) { private void sendErrorResponse(Session session, String errorMessage) {
try { try {
String message = String.format( String message = String.format(
"{\"type\":\"error\",\"message\":\"%s\"}", "{\"type\":\"error\",\"message\":\"%s\"}",
errorMessage errorMessage
); );
session.getBasicRemote().sendText(message); session.getBasicRemote().sendText(message);
} catch (Exception e) { } catch (Exception e) {
log.error("Impossible d'envoyer la réponse d'erreur", e); log.error("Impossible d'envoyer la réponse d'erreur", e);
} }
} }
/** /**
* Ferme une session WebSocket. * Ferme une session WebSocket.
*/ */
private void closeSession(Session session, String reason) { private void closeSession(Session session, String reason) {
try { try {
session.close(new CloseReason( session.close(new CloseReason(
CloseReason.CloseCodes.NORMAL_CLOSURE, CloseReason.CloseCodes.NORMAL_CLOSURE,
reason reason
)); ));
} catch (Exception e) { } catch (Exception e) {
log.warn("Erreur lors de la fermeture de la session", e); log.warn("Erreur lors de la fermeture de la session", e);
} }
} }
/** /**
* Nettoie les ressources d'une session. * Nettoie les ressources d'une session.
*/ */
private void cleanupSession(Session session) { private void cleanupSession(Session session) {
try { try {
session.close(); session.close();
} catch (Exception e) { } catch (Exception e) {
log.warn("Erreur lors du nettoyage de la session", e); log.warn("Erreur lors du nettoyage de la session", e);
} }
} }
/** /**
* Récupère le nombre de clients connectés. * Récupère le nombre de clients connectés.
*/ */
public int getConnectedClientsCount() { public int getConnectedClientsCount() {
return sessions.size(); return sessions.size();
} }
} }

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