Compare commits

2 Commits

128 changed files with 17510 additions and 9557 deletions

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
# Dockerfile for lionsdev-client-impl-quarkus
# Used by lionsctl pipeline. Expects `mvn clean package -Pprod` to have produced target/quarkus-app/ (fast-jar).
FROM registry.access.redhat.com/ubi8/openjdk-21:1.21
ENV LANGUAGE='en_US:en'
COPY --chown=1001:1001 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001:1001 target/quarkus-app/*.jar /deployments/
COPY --chown=1001:1001 target/quarkus-app/app/ /deployments/app/
COPY --chown=1001:1001 target/quarkus-app/quarkus/ /deployments/quarkus/
USER 1001
EXPOSE 8080
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ]

34
pom.xml
View File

@@ -14,9 +14,9 @@
<!-- Versions -->
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<myfaces.version>4.0.1</myfaces.version>
<primefaces.version>13.0.5</primefaces.version>
<quarkus.platform.version>3.7.3</quarkus.platform.version>
<myfaces.version>4.0.2</myfaces.version>
<primefaces.version>14.0.0</primefaces.version>
<quarkus.platform.version>3.27.3</quarkus.platform.version>
<lombok.version>1.18.32</lombok.version>
<jackson.version>2.17.0</jackson.version>
@@ -85,12 +85,23 @@
<dependency>
<groupId>io.quarkiverse.primefaces</groupId>
<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>
<groupId>org.apache.myfaces.core.extensions.quarkus</groupId>
<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>
<!-- Persistence -->
@@ -123,6 +134,13 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-mailer</artifactId>
</dependency>
<!-- PDF Generation -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets</artifactId>
@@ -197,7 +215,11 @@
</dependency>
<dependency>
<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>
<!-- Sécurité -->
<dependency>

View File

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

View File

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

View File

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

View File

@@ -1,61 +1,61 @@
# Configuration Prometheus corrigée
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'lions-portal-monitor'
rule_files:
- "rules/*rules.yml"
- "alerts/*alerts.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- 'alertmanager:9093'
scrape_configs:
# Application Quarkus metrics
- job_name: 'quarkus'
metrics_path: '/q/metrics'
static_configs:
- targets: ['quarkus-app:8080']
scrape_interval: 10s
# Postgres Exporter metrics
- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']
# Nginx metrics
- job_name: 'nginx'
static_configs:
- targets: ['nginx-exporter:9113']
# Node Exporter metrics (system metrics)
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
# Prometheus self-monitoring
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Nginx Status Page
- job_name: 'nginx-status'
metrics_path: /stub_status
static_configs:
- targets: ['nginx:80']
# Grafana metrics
- job_name: 'grafana'
static_configs:
- targets: ['grafana:3000']
# Node Exporter Host metrics
- job_name: 'node_exporter_host'
static_configs:
- targets: ['node-exporter:9100']
labels:
instance: 'host'
# Configuration Prometheus corrigée
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'lions-portal-monitor'
rule_files:
- "rules/*rules.yml"
- "alerts/*alerts.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- 'alertmanager:9093'
scrape_configs:
# Application Quarkus metrics
- job_name: 'quarkus'
metrics_path: '/q/metrics'
static_configs:
- targets: ['quarkus-app:8080']
scrape_interval: 10s
# Postgres Exporter metrics
- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']
# Nginx metrics
- job_name: 'nginx'
static_configs:
- targets: ['nginx-exporter:9113']
# Node Exporter metrics (system metrics)
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
# Prometheus self-monitoring
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Nginx Status Page
- job_name: 'nginx-status'
metrics_path: /stub_status
static_configs:
- targets: ['nginx:80']
# Grafana metrics
- job_name: 'grafana'
static_configs:
- targets: ['grafana:3000']
# Node Exporter Host metrics
- job_name: 'node_exporter_host'
static_configs:
- targets: ['node-exporter:9100']
labels:
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;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.primefaces.model.charts.*;
import org.primefaces.model.charts.bar.*;
import org.primefaces.model.charts.line.*;
import org.primefaces.model.charts.pie.*;
import org.primefaces.model.charts.optionconfig.title.Title;
import org.primefaces.model.charts.optionconfig.legend.Legend;
import java.io.Serializable;
import java.util.*;
/**
* Composant de gestion des graphiques.
* Fournit des modèles pour les graphiques linéaires, en barres et circulaires.
*
* @author Lions Dev Team
* @version 2.1
*/
@Slf4j
@Named
@ViewScoped
public class ChartComponent implements Serializable {
private static final long serialVersionUID = 1L;
private static final List<String> CHART_COLORS = Arrays.asList(
"rgba(33, 150, 243, 0.8)",
"rgba(255, 64, 129, 0.8)",
"rgba(255, 193, 7, 0.8)",
"rgba(76, 175, 80, 0.8)",
"rgba(156, 39, 176, 0.8)"
);
@Getter @Setter
private LineChartModel lineModel;
@Getter @Setter
private BarChartModel barModel;
@Getter @Setter
private PieChartModel pieModel;
/**
* Initialise les modèles de graphiques lors de la construction du composant.
*/
@PostConstruct
public void init() {
log.info("Initialisation des modèles de graphiques.");
createLineModel();
createBarModel();
createPieModel();
}
/**
* Crée un modèle de graphique linéaire.
*/
private void createLineModel() {
lineModel = new LineChartModel();
ChartData data = new ChartData();
LineChartDataSet dataSet = new LineChartDataSet();
dataSet.setLabel("Évolution des ventes");
dataSet.setData(new ArrayList<>(generateRandomData(6)));
dataSet.setBorderColor(CHART_COLORS.get(0));
dataSet.setFill(false);
dataSet.setTension(0.4);
data.addChartDataSet(dataSet);
data.setLabels(generateLabels(6, "Mois"));
lineModel.setData(data);
addChartOptions(lineModel, "Évolution temporelle");
log.debug("Modèle de graphique linéaire créé.");
}
/**
* Crée un modèle de graphique en barres.
*/
private void createBarModel() {
barModel = new BarChartModel();
ChartData data = new ChartData();
BarChartDataSet dataSet = new BarChartDataSet();
dataSet.setLabel("Performance par trimestre");
dataSet.setData(new ArrayList<>(generateRandomData(4)));
dataSet.setBackgroundColor(CHART_COLORS.get(1));
data.addChartDataSet(dataSet);
data.setLabels(generateLabels(4, "T"));
barModel.setData(data);
addChartOptions(barModel, "Performance trimestrielle");
log.debug("Modèle de graphique en barres créé.");
}
/**
* Crée un modèle de graphique circulaire.
*/
private void createPieModel() {
pieModel = new PieChartModel();
ChartData data = new ChartData();
PieChartDataSet dataSet = new PieChartDataSet();
dataSet.setData(Arrays.asList(25, 35, 40));
dataSet.setBackgroundColor(CHART_COLORS);
data.addChartDataSet(dataSet);
data.setLabels(Arrays.asList("Développement", "Marketing", "Infrastructure"));
pieModel.setData(data);
addChartOptions(pieModel, "Répartition des activités");
log.debug("Modèle de graphique circulaire créé.");
}
/**
* Ajoute des options telles que le titre et la légende aux graphiques.
*
* @param model Le modèle de graphique.
* @param title Titre du graphique.
*/
private void addChartOptions(ChartModel model, String title) {
Title chartTitle = new Title();
chartTitle.setDisplay(true);
chartTitle.setText(title);
Legend legend = new Legend();
legend.setDisplay(true);
legend.setPosition("bottom");
if (model instanceof LineChartModel) {
LineChartModel lineChart = (LineChartModel) model;
lineChart.setExtender((String) chartTitle.getText());
} else if (model instanceof BarChartModel) {
BarChartModel barChart = (BarChartModel) model;
barChart.setExtender((String) chartTitle.getText());
} else if (model instanceof PieChartModel) {
PieChartModel pieChart = (PieChartModel) model;
pieChart.setExtender((String) chartTitle.getText());
}
}
private List<Number> generateRandomData(int size) {
Random random = new Random();
List<Number> data = new ArrayList<>();
for (int i = 0; i < size; i++) {
data.add(random.nextInt(100));
}
return data;
}
private List<String> generateLabels(int size, String prefix) {
List<String> labels = new ArrayList<>();
for (int i = 1; i <= size; i++) {
labels.add(prefix + " " + i);
}
return labels;
}
}
package dev.lions.components;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.primefaces.model.charts.*;
import org.primefaces.model.charts.bar.*;
import org.primefaces.model.charts.line.*;
import org.primefaces.model.charts.pie.*;
import org.primefaces.model.charts.optionconfig.title.Title;
import org.primefaces.model.charts.optionconfig.legend.Legend;
import java.io.Serializable;
import java.util.*;
/**
* Composant de gestion des graphiques.
* Fournit des modèles pour les graphiques linéaires, en barres et circulaires.
*
* @author Lions Dev Team
* @version 2.1
*/
@Slf4j
@Named
@ViewScoped
public class ChartComponent implements Serializable {
private static final long serialVersionUID = 1L;
private static final List<String> CHART_COLORS = Arrays.asList(
"rgba(33, 150, 243, 0.8)",
"rgba(255, 64, 129, 0.8)",
"rgba(255, 193, 7, 0.8)",
"rgba(76, 175, 80, 0.8)",
"rgba(156, 39, 176, 0.8)"
);
@Getter @Setter
private LineChartModel lineModel;
@Getter @Setter
private BarChartModel barModel;
@Getter @Setter
private PieChartModel pieModel;
/**
* Initialise les modèles de graphiques lors de la construction du composant.
*/
@PostConstruct
public void init() {
log.info("Initialisation des modèles de graphiques.");
createLineModel();
createBarModel();
createPieModel();
}
/**
* Crée un modèle de graphique linéaire.
*/
private void createLineModel() {
lineModel = new LineChartModel();
ChartData data = new ChartData();
LineChartDataSet dataSet = new LineChartDataSet();
dataSet.setLabel("Évolution des ventes");
dataSet.setData(new ArrayList<>(generateRandomData(6)));
dataSet.setBorderColor(CHART_COLORS.get(0));
dataSet.setFill(false);
dataSet.setTension(0.4);
data.addChartDataSet(dataSet);
data.setLabels(generateLabels(6, "Mois"));
lineModel.setData(data);
addChartOptions(lineModel, "Évolution temporelle");
log.debug("Modèle de graphique linéaire créé.");
}
/**
* Crée un modèle de graphique en barres.
*/
private void createBarModel() {
barModel = new BarChartModel();
ChartData data = new ChartData();
BarChartDataSet dataSet = new BarChartDataSet();
dataSet.setLabel("Performance par trimestre");
dataSet.setData(new ArrayList<>(generateRandomData(4)));
dataSet.setBackgroundColor(CHART_COLORS.get(1));
data.addChartDataSet(dataSet);
data.setLabels(generateLabels(4, "T"));
barModel.setData(data);
addChartOptions(barModel, "Performance trimestrielle");
log.debug("Modèle de graphique en barres créé.");
}
/**
* Crée un modèle de graphique circulaire.
*/
private void createPieModel() {
pieModel = new PieChartModel();
ChartData data = new ChartData();
PieChartDataSet dataSet = new PieChartDataSet();
dataSet.setData(Arrays.asList(25, 35, 40));
dataSet.setBackgroundColor(CHART_COLORS);
data.addChartDataSet(dataSet);
data.setLabels(Arrays.asList("Développement", "Marketing", "Infrastructure"));
pieModel.setData(data);
addChartOptions(pieModel, "Répartition des activités");
log.debug("Modèle de graphique circulaire créé.");
}
/**
* Ajoute des options telles que le titre et la légende aux graphiques.
*
* @param model Le modèle de graphique.
* @param title Titre du graphique.
*/
private void addChartOptions(ChartModel model, String title) {
Title chartTitle = new Title();
chartTitle.setDisplay(true);
chartTitle.setText(title);
Legend legend = new Legend();
legend.setDisplay(true);
legend.setPosition("bottom");
if (model instanceof LineChartModel) {
LineChartModel lineChart = (LineChartModel) model;
lineChart.setExtender((String) chartTitle.getText());
} else if (model instanceof BarChartModel) {
BarChartModel barChart = (BarChartModel) model;
barChart.setExtender((String) chartTitle.getText());
} else if (model instanceof PieChartModel) {
PieChartModel pieChart = (PieChartModel) model;
pieChart.setExtender((String) chartTitle.getText());
}
}
private List<Number> generateRandomData(int size) {
Random random = new Random();
List<Number> data = new ArrayList<>();
for (int i = 0; i < size; i++) {
data.add(random.nextInt(100));
}
return data;
}
private List<String> generateLabels(int size, String prefix) {
List<String> labels = new ArrayList<>();
for (int i = 1; i <= size; i++) {
labels.add(prefix + " " + i);
}
return labels;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,23 @@
package dev.lions.events;
import dev.lions.models.Contact;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.AllArgsConstructor;
/**
* É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.
*/
@Getter
@AllArgsConstructor
public class ContactSubmissionEvent {
private final Contact contact;
private final LocalDateTime timestamp;
public ContactSubmissionEvent(Contact contact) {
this.contact = contact;
this.timestamp = LocalDateTime.now();
}
}
package dev.lions.events;
import dev.lions.models.Contact;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.AllArgsConstructor;
/**
* É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.
*/
@Getter
@AllArgsConstructor
public class ContactSubmissionEvent {
private final Contact contact;
private final LocalDateTime timestamp;
public ContactSubmissionEvent(Contact contact) {
this.contact = contact;
this.timestamp = LocalDateTime.now();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,42 +1,42 @@
package dev.lions.events;
import java.util.Map;
/**
* É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.
*/
public class StorageEvent {
private final String type;
private final Map<String, Object> data;
/**
* Crée une nouvelle instance de StorageEvent.
*
* @param type Type de l'événement de stockage
* @param data Données associées à l'événement
*/
public StorageEvent(String type, Map<String, Object> data) {
this.type = type;
this.data = data;
}
/**
* Récupère le type de l'événement de stockage.
*
* @return Type de l'événement
*/
public String getType() {
return type;
}
/**
* Récupère les données associées à l'événement de stockage.
*
* @return Données de l'événement
*/
public Map<String, Object> getData() {
return data;
}
package dev.lions.events;
import java.util.Map;
/**
* É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.
*/
public class StorageEvent {
private final String type;
private final Map<String, Object> data;
/**
* Crée une nouvelle instance de StorageEvent.
*
* @param type Type de l'événement de stockage
* @param data Données associées à l'événement
*/
public StorageEvent(String type, Map<String, Object> data) {
this.type = type;
this.data = data;
}
/**
* Récupère le type de l'événement de stockage.
*
* @return Type de l'événement
*/
public String getType() {
return type;
}
/**
* Récupère les données associées à l'événement de stockage.
*
* @return Données de l'événement
*/
public Map<String, Object> getData() {
return data;
}
}

View File

@@ -1,17 +1,17 @@
package dev.lions.exceptions;
/**
* 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
* publication des événements d'analyse.
*/
public class AnalyticsException extends RuntimeException {
public AnalyticsException(String message) {
super(message);
}
public AnalyticsException(String message, Throwable cause) {
super(message, cause);
}
package dev.lions.exceptions;
/**
* 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
* publication des événements d'analyse.
*/
public class AnalyticsException extends RuntimeException {
public AnalyticsException(String message) {
super(message);
}
public AnalyticsException(String message, Throwable cause) {
super(message, cause);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,21 @@
package dev.lions.exceptions;
/**
* 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.
*/
public class EventProcessingException extends RuntimeException {
private static final long serialVersionUID = 1L;
public EventProcessingException(String message) {
super(message);
}
public EventProcessingException(String message, Throwable cause) {
super(message, cause);
}
public EventProcessingException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
package dev.lions.exceptions;
/**
* 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.
*/
public class EventProcessingException extends RuntimeException {
private static final long serialVersionUID = 1L;
public EventProcessingException(String message) {
super(message);
}
public EventProcessingException(String message, Throwable cause) {
super(message, cause);
}
public EventProcessingException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,59 +1,59 @@
package dev.lions.models;
import lombok.Getter;
/**
* 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
* un indicateur de criticité.
*/
public enum NotificationType {
NEW_CONTACT(true, "Nouveau contact", "Un nouveau contact a été reçu"),
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"),
COMMENT_ADDED(false, "Nouveau commentaire", "Un commentaire a été ajouté"),
DEADLINE_APPROACHING(true, "Échéance proche", "Une échéance approche"),
SYSTEM_ALERT(true, "Alerte système", "Une alerte système requiert votre attention"),
MAINTENANCE_SCHEDULED(false, "Maintenance planifiée", "Une maintenance est planifiée"),
USER_MENTION(true, "Mention", "Vous avez été mentionné"),
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");
@Getter
private final String title;
@Getter
private final String defaultMessage;
private final boolean isCritical;
/**
* Constructeur privé pour créer une instance de NotificationType.
*
* @param isCritical Indicateur de criticité de la notification
* @param title Titre de la notification
* @param defaultMessage Message par défaut de la notification
*/
private NotificationType(boolean isCritical, String title, String defaultMessage) {
this.isCritical = isCritical;
this.title = title;
this.defaultMessage = defaultMessage;
}
/**
* 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
*/
public String getNotificationTemplate() {
return String.format("%s : %s", this.title, this.defaultMessage);
}
/**
* Indique si le type de notification est critique.
* Les notifications critiques nécessitent généralement une attention prioritaire.
*
* @return true si le type de notification est critique
*/
public boolean isCritical() {
return isCritical;
}
package dev.lions.models;
import lombok.Getter;
/**
* 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
* un indicateur de criticité.
*/
public enum NotificationType {
NEW_CONTACT(true, "Nouveau contact", "Un nouveau contact a été reçu"),
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"),
COMMENT_ADDED(false, "Nouveau commentaire", "Un commentaire a été ajouté"),
DEADLINE_APPROACHING(true, "Échéance proche", "Une échéance approche"),
SYSTEM_ALERT(true, "Alerte système", "Une alerte système requiert votre attention"),
MAINTENANCE_SCHEDULED(false, "Maintenance planifiée", "Une maintenance est planifiée"),
USER_MENTION(true, "Mention", "Vous avez été mentionné"),
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");
@Getter
private final String title;
@Getter
private final String defaultMessage;
private final boolean isCritical;
/**
* Constructeur privé pour créer une instance de NotificationType.
*
* @param isCritical Indicateur de criticité de la notification
* @param title Titre de la notification
* @param defaultMessage Message par défaut de la notification
*/
private NotificationType(boolean isCritical, String title, String defaultMessage) {
this.isCritical = isCritical;
this.title = title;
this.defaultMessage = defaultMessage;
}
/**
* 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
*/
public String getNotificationTemplate() {
return String.format("%s : %s", this.title, this.defaultMessage);
}
/**
* Indique si le type de notification est critique.
* Les notifications critiques nécessitent généralement une attention prioritaire.
*
* @return true si le type de notification est critique
*/
public boolean isCritical() {
return isCritical;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,150 +1,150 @@
package dev.lions.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.proxy.HibernateProxy;
/**
* Représente un témoignage de client pour un projet.
*/
@Entity
@Table(name = "project_testimonials")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Getter
@Setter
@ToString
@Builder
@AllArgsConstructor
public class Testimonial implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Id
@NotBlank(message = "L'identifiant du témoignage est obligatoire")
private String id;
@NotBlank(message = "Le nom du client est obligatoire")
@Size(min = 3, max = 100)
private String clientName;
@NotBlank(message = "Le poste du client est obligatoire")
@Size(min = 3, max = 100)
private String clientPosition;
@NotBlank(message = "Le contenu du témoignage est obligatoire")
@Size(min = 10, max = 1000)
private String content;
@Size(max = 255)
private String clientImage;
@ManyToOne
@JoinColumn(name = "project_id", nullable = false)
private Project project;
@Builder.Default
private int rating = 5;
@Builder.Default
private boolean isFeatured = false;
@Column(name = "completion_date")
private LocalDateTime completionDate;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
/**
* Constructeur par défaut requis pour JPA.
*/
public Testimonial() {
this.createdAt = LocalDateTime.now();
}
/**
* Crée une nouvelle instance de témoignage.
*/
public static Testimonial create(String clientName, String clientPosition, String content,
String clientImage, Project project, int rating, boolean isFeatured) {
Testimonial testimonial = new Testimonial();
testimonial.setClientName(clientName);
testimonial.setClientPosition(clientPosition);
testimonial.setContent(content);
testimonial.setClientImage(clientImage);
testimonial.setProject(project);
testimonial.setRating(rating);
testimonial.setFeatured(isFeatured);
testimonial.setCreatedAt(LocalDateTime.now());
return testimonial;
}
/**
* Builder personnalisé pour permettre d'ajouter un titre de projet.
*/
public static class TestimonialBuilder {
public TestimonialBuilder projectTitle(String title) {
if (this.project == null) {
this.project = new Project();
}
this.project.setTitle(title);
return this;
}
public TestimonialBuilder date(LocalDateTime completionDate) {
this.completionDate = completionDate;
return this;
}
}
@Override
public final boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null) {
return false;
}
Class<?> oEffectiveClass =
o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer()
.getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass =
this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
.getPersistentClass()
: this.getClass();
if (thisEffectiveClass != oEffectiveClass) {
return false;
}
Testimonial that = (Testimonial) o;
return getId() != null && Objects.equals(getId(), that.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
.getPersistentClass().hashCode()
: getClass().hashCode();
}
}
package dev.lions.models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.proxy.HibernateProxy;
/**
* Représente un témoignage de client pour un projet.
*/
@Entity
@Table(name = "project_testimonials")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@Getter
@Setter
@ToString
@Builder
@AllArgsConstructor
public class Testimonial implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Id
@NotBlank(message = "L'identifiant du témoignage est obligatoire")
private String id;
@NotBlank(message = "Le nom du client est obligatoire")
@Size(min = 3, max = 100)
private String clientName;
@NotBlank(message = "Le poste du client est obligatoire")
@Size(min = 3, max = 100)
private String clientPosition;
@NotBlank(message = "Le contenu du témoignage est obligatoire")
@Size(min = 10, max = 1000)
private String content;
@Size(max = 255)
private String clientImage;
@ManyToOne
@JoinColumn(name = "project_id", nullable = false)
private Project project;
@Builder.Default
private int rating = 5;
@Builder.Default
private boolean isFeatured = false;
@Column(name = "completion_date")
private LocalDateTime completionDate;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
/**
* Constructeur par défaut requis pour JPA.
*/
public Testimonial() {
this.createdAt = LocalDateTime.now();
}
/**
* Crée une nouvelle instance de témoignage.
*/
public static Testimonial create(String clientName, String clientPosition, String content,
String clientImage, Project project, int rating, boolean isFeatured) {
Testimonial testimonial = new Testimonial();
testimonial.setClientName(clientName);
testimonial.setClientPosition(clientPosition);
testimonial.setContent(content);
testimonial.setClientImage(clientImage);
testimonial.setProject(project);
testimonial.setRating(rating);
testimonial.setFeatured(isFeatured);
testimonial.setCreatedAt(LocalDateTime.now());
return testimonial;
}
/**
* Builder personnalisé pour permettre d'ajouter un titre de projet.
*/
public static class TestimonialBuilder {
public TestimonialBuilder projectTitle(String title) {
if (this.project == null) {
this.project = new Project();
}
this.project.setTitle(title);
return this;
}
public TestimonialBuilder date(LocalDateTime completionDate) {
this.completionDate = completionDate;
return this;
}
}
@Override
public final boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null) {
return false;
}
Class<?> oEffectiveClass =
o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer()
.getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass =
this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
.getPersistentClass()
: this.getClass();
if (thisEffectiveClass != oEffectiveClass) {
return false;
}
Testimonial that = (Testimonial) o;
return getId() != null && Objects.equals(getId(), that.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer()
.getPersistentClass().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;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import dev.lions.events.AnalyticsEvent;
import dev.lions.exceptions.RepositoryException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Repository gérant la persistance des événements analytiques.
* Cette classe assure le stockage, la récupération et l'analyse
* des événements analytiques de l'application.
*
* @author Lions Dev Team
* @version 1.1
*/
@Slf4j
@ApplicationScoped
public class AnalyticsRepository extends BaseRepository<AnalyticsEvent, Long> {
@PersistenceContext
private EntityManager entityManager;
/**
* Recherche tous les événements pour une période donnée.
*
* @param startDate Date de début de la période
* @param endDate Date de fin de la période
* @return Liste des événements trouvés
* @throws RepositoryException En cas d'erreur de requête
*/
public List<AnalyticsEvent> findEventsByDateRange(@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) {
log.debug("Recherche des événements entre {} et {}", startDate, endDate);
try {
TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
"SELECT e FROM AnalyticsEvent e " +
"WHERE e.timestamp BETWEEN :startDate AND :endDate " +
"ORDER BY e.timestamp DESC",
AnalyticsEvent.class);
query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate);
List<AnalyticsEvent> events = query.getResultList();
log.info("Trouvé {} événements pour la période demandée", events.size());
return events;
} catch (Exception e) {
log.error("Erreur lors de la recherche des événements par période", e);
throw new RepositoryException(
"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.
*
* @param eventType Type d'événement recherché
* @param startDate Date de début
* @param endDate Date de fin
* @return Liste des événements trouvés
*/
public List<AnalyticsEvent> findEventsByType(@NotNull String eventType,
@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) {
log.debug("Recherche des événements de type {} entre {} et {}",
eventType, startDate, endDate);
try {
TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
"SELECT e FROM AnalyticsEvent e " +
"WHERE e.eventType = :eventType " +
"AND e.timestamp BETWEEN :startDate AND :endDate " +
"ORDER BY e.timestamp DESC",
AnalyticsEvent.class);
query.setParameter("eventType", eventType);
query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des événements par type", e);
}
}
/**
* Enregistre un nouvel événement analytique.
*
* @param event Événement à sauvegarder
* @return Événement sauvegardé
*/
@Transactional
public AnalyticsEvent save(AnalyticsEvent event) {
log.debug("Sauvegarde d'un nouvel événement de type: {}", event.getEventType());
try {
if (event.getId() == null) {
entityManager.persist(event);
log.info("Nouvel événement créé avec l'ID: {}", event.getId());
} else {
event = entityManager.merge(event);
log.info("Événement mis à jour avec l'ID: {}", event.getId());
}
return event;
} catch (Exception 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);
}
}
/**
* Calcule le nombre d'événements par type pour une période donnée.
*
* @param startDate Date de début
* @param endDate Date de fin
* @return Map des compteurs par type d'événement
*/
public Map<String, Long> getEventCountByType(@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) {
log.debug("Calcul du nombre d'événements entre {} et {}", startDate, endDate);
try {
List<Object[]> results = entityManager.createQuery(
"SELECT e.eventType, COUNT(e) FROM AnalyticsEvent e " +
"WHERE e.timestamp BETWEEN :startDate AND :endDate " +
"GROUP BY e.eventType ORDER BY COUNT(e) DESC",
Object[].class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDate)
.getResultList();
return results.stream()
.collect(Collectors.toMap(
row -> (String) row[0],
row -> (Long) row[1]
));
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors du calcul du nombre d'événements", e);
}
}
/**
* Recherche les événements associés à un contact spécifique.
*
* @param contactId Identifiant du contact
* @return Liste des événements associés
*/
public List<AnalyticsEvent> findEventsByContactId(@NotNull String contactId) {
log.debug("Recherche des événements pour le contact {}", contactId);
try {
TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
"SELECT e FROM AnalyticsEvent e " +
"WHERE e.contactId = :contactId " +
"ORDER BY e.timestamp DESC",
AnalyticsEvent.class);
query.setParameter("contactId", contactId);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des événements par contact", e);
}
}
/**
* Supprime les événements antérieurs à une date donnée.
*
* @param retentionDate Date limite de conservation
* @return Nombre d'événements supprimés
*/
@Transactional
public int deleteEventsOlderThan(@NotNull LocalDateTime retentionDate) {
log.info("Suppression des événements antérieurs à {}", retentionDate);
try {
int deletedCount = entityManager.createQuery(
"DELETE FROM AnalyticsEvent e WHERE e.timestamp < :retentionDate")
.setParameter("retentionDate", retentionDate)
.executeUpdate();
log.info("{} événements supprimés", deletedCount);
return deletedCount;
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la suppression des anciens événements", e);
}
}
/**
* Recherche un événement par son identifiant.
*
* @param id Identifiant de l'événement
* @return Optional contenant l'événement s'il existe
*/
public Optional<AnalyticsEvent> findById(Long id) {
log.debug("Recherche de l'événement avec l'ID: {}", id);
try {
AnalyticsEvent event = entityManager.find(AnalyticsEvent.class, id);
return Optional.ofNullable(event);
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche de l'événement par ID", e);
}
}
/**
* Calcule les statistiques d'événements par environnement.
*
* @param startDate Date de début
* @param endDate Date de fin
* @return Map des statistiques par environnement
*/
public Map<String, Long> getEventStatistics(LocalDateTime startDate,
LocalDateTime endDate) {
log.debug("Calcul des statistiques entre {} et {}", startDate, endDate);
try {
List<Object[]> results = entityManager.createQuery(
"SELECT e.environment, COUNT(e) FROM AnalyticsEvent e " +
"WHERE e.timestamp BETWEEN :startDate AND :endDate " +
"GROUP BY e.environment",
Object[].class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDate)
.getResultList();
return results.stream()
.collect(Collectors.toMap(
row -> (String) row[0],
row -> (Long) row[1]
));
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors du calcul des statistiques", e);
}
}
package dev.lions.repositories;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import dev.lions.events.AnalyticsEvent;
import dev.lions.exceptions.RepositoryException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Repository gérant la persistance des événements analytiques.
* Cette classe assure le stockage, la récupération et l'analyse
* des événements analytiques de l'application.
*
* @author Lions Dev Team
* @version 1.1
*/
@Slf4j
@ApplicationScoped
public class AnalyticsRepository extends BaseRepository<AnalyticsEvent, Long> {
@PersistenceContext
private EntityManager entityManager;
/**
* Recherche tous les événements pour une période donnée.
*
* @param startDate Date de début de la période
* @param endDate Date de fin de la période
* @return Liste des événements trouvés
* @throws RepositoryException En cas d'erreur de requête
*/
public List<AnalyticsEvent> findEventsByDateRange(@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) {
log.debug("Recherche des événements entre {} et {}", startDate, endDate);
try {
TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
"SELECT e FROM AnalyticsEvent e " +
"WHERE e.timestamp BETWEEN :startDate AND :endDate " +
"ORDER BY e.timestamp DESC",
AnalyticsEvent.class);
query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate);
List<AnalyticsEvent> events = query.getResultList();
log.info("Trouvé {} événements pour la période demandée", events.size());
return events;
} catch (Exception e) {
log.error("Erreur lors de la recherche des événements par période", e);
throw new RepositoryException(
"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.
*
* @param eventType Type d'événement recherché
* @param startDate Date de début
* @param endDate Date de fin
* @return Liste des événements trouvés
*/
public List<AnalyticsEvent> findEventsByType(@NotNull String eventType,
@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) {
log.debug("Recherche des événements de type {} entre {} et {}",
eventType, startDate, endDate);
try {
TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
"SELECT e FROM AnalyticsEvent e " +
"WHERE e.eventType = :eventType " +
"AND e.timestamp BETWEEN :startDate AND :endDate " +
"ORDER BY e.timestamp DESC",
AnalyticsEvent.class);
query.setParameter("eventType", eventType);
query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des événements par type", e);
}
}
/**
* Enregistre un nouvel événement analytique.
*
* @param event Événement à sauvegarder
* @return Événement sauvegardé
*/
@Transactional
public AnalyticsEvent save(AnalyticsEvent event) {
log.debug("Sauvegarde d'un nouvel événement de type: {}", event.getEventType());
try {
if (event.getId() == null) {
entityManager.persist(event);
log.info("Nouvel événement créé avec l'ID: {}", event.getId());
} else {
event = entityManager.merge(event);
log.info("Événement mis à jour avec l'ID: {}", event.getId());
}
return event;
} catch (Exception 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);
}
}
/**
* Calcule le nombre d'événements par type pour une période donnée.
*
* @param startDate Date de début
* @param endDate Date de fin
* @return Map des compteurs par type d'événement
*/
public Map<String, Long> getEventCountByType(@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) {
log.debug("Calcul du nombre d'événements entre {} et {}", startDate, endDate);
try {
List<Object[]> results = entityManager.createQuery(
"SELECT e.eventType, COUNT(e) FROM AnalyticsEvent e " +
"WHERE e.timestamp BETWEEN :startDate AND :endDate " +
"GROUP BY e.eventType ORDER BY COUNT(e) DESC",
Object[].class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDate)
.getResultList();
return results.stream()
.collect(Collectors.toMap(
row -> (String) row[0],
row -> (Long) row[1]
));
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors du calcul du nombre d'événements", e);
}
}
/**
* Recherche les événements associés à un contact spécifique.
*
* @param contactId Identifiant du contact
* @return Liste des événements associés
*/
public List<AnalyticsEvent> findEventsByContactId(@NotNull String contactId) {
log.debug("Recherche des événements pour le contact {}", contactId);
try {
TypedQuery<AnalyticsEvent> query = entityManager.createQuery(
"SELECT e FROM AnalyticsEvent e " +
"WHERE e.contactId = :contactId " +
"ORDER BY e.timestamp DESC",
AnalyticsEvent.class);
query.setParameter("contactId", contactId);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des événements par contact", e);
}
}
/**
* Supprime les événements antérieurs à une date donnée.
*
* @param retentionDate Date limite de conservation
* @return Nombre d'événements supprimés
*/
@Transactional
public int deleteEventsOlderThan(@NotNull LocalDateTime retentionDate) {
log.info("Suppression des événements antérieurs à {}", retentionDate);
try {
int deletedCount = entityManager.createQuery(
"DELETE FROM AnalyticsEvent e WHERE e.timestamp < :retentionDate")
.setParameter("retentionDate", retentionDate)
.executeUpdate();
log.info("{} événements supprimés", deletedCount);
return deletedCount;
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la suppression des anciens événements", e);
}
}
/**
* Recherche un événement par son identifiant.
*
* @param id Identifiant de l'événement
* @return Optional contenant l'événement s'il existe
*/
public Optional<AnalyticsEvent> findById(Long id) {
log.debug("Recherche de l'événement avec l'ID: {}", id);
try {
AnalyticsEvent event = entityManager.find(AnalyticsEvent.class, id);
return Optional.ofNullable(event);
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche de l'événement par ID", e);
}
}
/**
* Calcule les statistiques d'événements par environnement.
*
* @param startDate Date de début
* @param endDate Date de fin
* @return Map des statistiques par environnement
*/
public Map<String, Long> getEventStatistics(LocalDateTime startDate,
LocalDateTime endDate) {
log.debug("Calcul des statistiques entre {} et {}", startDate, endDate);
try {
List<Object[]> results = entityManager.createQuery(
"SELECT e.environment, COUNT(e) FROM AnalyticsEvent e " +
"WHERE e.timestamp BETWEEN :startDate AND :endDate " +
"GROUP BY e.environment",
Object[].class)
.setParameter("startDate", startDate)
.setParameter("endDate", endDate)
.getResultList();
return results.stream()
.collect(Collectors.toMap(
row -> (String) row[0],
row -> (Long) row[1]
));
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors du calcul des statistiques", e);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,228 +1,228 @@
package dev.lions.repositories;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotEmpty;
import dev.lions.models.Project;
import dev.lions.exceptions.RepositoryException;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Repository gérant la persistance des projets dans l'application.
* Cette classe assure le stockage, la recherche et la gestion des projets
* avec support pour le filtrage par tags, dates et statuts.
*/
@Slf4j
@ApplicationScoped
public class ProjectRepository extends BaseRepository<Project, String> {
@PersistenceContext
private EntityManager entityManager;
/**
* Recherche les projets par ensemble de tags.
* Cette méthode permet de filtrer les projets qui contiennent au moins
* un des tags spécifiés.
*
* @param tags Liste des tags à rechercher
* @return Liste des projets correspondants
*/
public List<Project> findByTags(@NotEmpty Set<String> tags) {
log.debug("Recherche de projets par tags : {}", tags);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT DISTINCT p FROM Project p " +
"JOIN p.tags t " +
"WHERE t IN :tags",
Project.class
);
query.setParameter("tags", tags);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets par tags", e);
}
}
/**
* Recherche les projets mis en avant.
* Permet de récupérer les projets marqués comme "featured" pour
* l'affichage en page d'accueil.
*
* @param featured État de mise en avant recherché
* @return Liste des projets mis en avant
*/
public List<Project> findByFeatured(boolean featured) {
log.debug("Recherche des projets featured={}", featured);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " +
"WHERE p.featured = :featured " +
"ORDER BY p.completionDate DESC",
Project.class
);
query.setParameter("featured", featured);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets mis en avant", e);
}
}
/**
* Recherche les projets complétés dans une période donnée.
*
* @param startDate Date de début
* @param endDate Date de fin
* @return Liste des projets pour la période
*/
public List<Project> findByCompletionDateBetween(
@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) {
log.debug("Recherche des projets complétés entre {} et {}",
startDate, endDate);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " +
"WHERE p.completionDate BETWEEN :startDate AND :endDate " +
"ORDER BY p.completionDate DESC",
Project.class
);
query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets par période", e);
}
}
/**
* Récupère les projets les plus récents.
*
* @param limit Nombre maximum de projets à retourner
* @return Liste limitée des projets les plus récents
*/
public List<Project> findRecentProjects(int limit) {
log.debug("Recherche des {} projets les plus récents", limit);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " +
"ORDER BY p.completionDate DESC",
Project.class
);
query.setMaxResults(limit);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets récents", e);
}
}
/**
* Recherche les projets par technologie utilisée.
*
* @param technology Technologie recherchée
* @return Liste des projets utilisant cette technologie
*/
public List<Project> findByTechnology(@NotNull String technology) {
log.debug("Recherche des projets utilisant la technologie : {}",
technology);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " +
"JOIN p.technologies t " +
"WHERE LOWER(t) = LOWER(:technology)",
Project.class
);
query.setParameter("technology", technology.toLowerCase());
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets par technologie", e);
}
}
/**
* Met à jour le statut "featured" d'un projet.
*
* @param projectId Identifiant du projet
* @param featured Nouveau statut featured
*/
@Transactional
public void updateFeaturedStatus(@NotNull String projectId, boolean featured) {
log.debug("Mise à jour du statut featured={} pour le projet {}",
featured, projectId);
try {
Project project = findById(projectId)
.orElseThrow(() -> new RepositoryException("Projet non trouvé"));
project.setFeatured(featured);
update(project);
log.info("Statut featured mis à jour pour le projet {}", projectId);
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la mise à jour du statut featured", e);
}
}
/**
* Compte les projets par technologie.
*
* @return Nombre de projets par technologie
*/
public Map<String, Long> countByTechnology() {
log.debug("Comptage des projets par technologie");
try {
List<Object[]> results = entityManager.createQuery(
"SELECT t, COUNT(p) FROM Project p " +
"JOIN p.technologies t " +
"GROUP BY t",
Object[].class
).getResultList();
return results.stream()
.collect(Collectors.toMap(
row -> (String) row[0],
row -> (Long) row[1]
));
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors du comptage des projets par technologie", e);
}
}
package dev.lions.repositories;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotEmpty;
import dev.lions.models.Project;
import dev.lions.exceptions.RepositoryException;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Repository gérant la persistance des projets dans l'application.
* Cette classe assure le stockage, la recherche et la gestion des projets
* avec support pour le filtrage par tags, dates et statuts.
*/
@Slf4j
@ApplicationScoped
public class ProjectRepository extends BaseRepository<Project, String> {
@PersistenceContext
private EntityManager entityManager;
/**
* Recherche les projets par ensemble de tags.
* Cette méthode permet de filtrer les projets qui contiennent au moins
* un des tags spécifiés.
*
* @param tags Liste des tags à rechercher
* @return Liste des projets correspondants
*/
public List<Project> findByTags(@NotEmpty Set<String> tags) {
log.debug("Recherche de projets par tags : {}", tags);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT DISTINCT p FROM Project p " +
"JOIN p.tags t " +
"WHERE t IN :tags",
Project.class
);
query.setParameter("tags", tags);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets par tags", e);
}
}
/**
* Recherche les projets mis en avant.
* Permet de récupérer les projets marqués comme "featured" pour
* l'affichage en page d'accueil.
*
* @param featured État de mise en avant recherché
* @return Liste des projets mis en avant
*/
public List<Project> findByFeatured(boolean featured) {
log.debug("Recherche des projets featured={}", featured);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " +
"WHERE p.featured = :featured " +
"ORDER BY p.completionDate DESC",
Project.class
);
query.setParameter("featured", featured);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets mis en avant", e);
}
}
/**
* Recherche les projets complétés dans une période donnée.
*
* @param startDate Date de début
* @param endDate Date de fin
* @return Liste des projets pour la période
*/
public List<Project> findByCompletionDateBetween(
@NotNull LocalDateTime startDate,
@NotNull LocalDateTime endDate) {
log.debug("Recherche des projets complétés entre {} et {}",
startDate, endDate);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " +
"WHERE p.completionDate BETWEEN :startDate AND :endDate " +
"ORDER BY p.completionDate DESC",
Project.class
);
query.setParameter("startDate", startDate);
query.setParameter("endDate", endDate);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets par période", e);
}
}
/**
* Récupère les projets les plus récents.
*
* @param limit Nombre maximum de projets à retourner
* @return Liste limitée des projets les plus récents
*/
public List<Project> findRecentProjects(int limit) {
log.debug("Recherche des {} projets les plus récents", limit);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " +
"ORDER BY p.completionDate DESC",
Project.class
);
query.setMaxResults(limit);
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets récents", e);
}
}
/**
* Recherche les projets par technologie utilisée.
*
* @param technology Technologie recherchée
* @return Liste des projets utilisant cette technologie
*/
public List<Project> findByTechnology(@NotNull String technology) {
log.debug("Recherche des projets utilisant la technologie : {}",
technology);
try {
TypedQuery<Project> query = entityManager.createQuery(
"SELECT p FROM Project p " +
"JOIN p.technologies t " +
"WHERE LOWER(t) = LOWER(:technology)",
Project.class
);
query.setParameter("technology", technology.toLowerCase());
return query.getResultList();
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la recherche des projets par technologie", e);
}
}
/**
* Met à jour le statut "featured" d'un projet.
*
* @param projectId Identifiant du projet
* @param featured Nouveau statut featured
*/
@Transactional
public void updateFeaturedStatus(@NotNull String projectId, boolean featured) {
log.debug("Mise à jour du statut featured={} pour le projet {}",
featured, projectId);
try {
Project project = findById(projectId)
.orElseThrow(() -> new RepositoryException("Projet non trouvé"));
project.setFeatured(featured);
update(project);
log.info("Statut featured mis à jour pour le projet {}", projectId);
} catch (Exception e) {
throw new RepositoryException(
"Erreur lors de la mise à jour du statut featured", e);
}
}
/**
* Compte les projets par technologie.
*
* @return Nombre de projets par technologie
*/
public Map<String, Long> countByTechnology() {
log.debug("Comptage des projets par technologie");
try {
List<Object[]> results = entityManager.createQuery(
"SELECT t, COUNT(p) FROM Project p " +
"JOIN p.technologies t " +
"GROUP BY t",
Object[].class
).getResultList();
return results.stream()
.collect(Collectors.toMap(
row -> (String) row[0],
row -> (Long) row[1]
));
} catch (Exception e) {
throw new RepositoryException(
"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;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* Filtre de sécurité pour l'application.
* Implémente la logique de sécurité pour toutes les requêtes entrantes.
*
* @author Lions Dev Team
* @version 1.0
*/
@Slf4j
@ApplicationScoped
@WebFilter(urlPatterns = "/*")
public class SecurityFilter implements Filter {
/**
* Initialise le filtre.
* Cette méthode est appelée par le conteneur lors du démarrage.
*
* @param filterConfig Configuration du filtre
*/
@Override
public void init(FilterConfig filterConfig) {
log.info("Initialisation du filtre de sécurité");
}
/**
* Applique la logique de filtrage sur chaque requête.
*
* @param request La requête entrante
* @param response La réponse
* @param chain La chaîne de filtres
* @throws IOException En cas d'erreur d'entrée/sortie
* @throws ServletException En cas d'erreur de servlet
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestUri = httpRequest.getRequestURI();
log.debug("Traitement de la requête: {}", requestUri);
try {
// Vérification de sécurité de base
if (isSecurityCheckPassed(httpRequest)) {
chain.doFilter(request, response);
} else {
log.warn("Accès refusé pour la requête: {}", requestUri);
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Accès refusé");
}
} catch (Exception e) {
log.error("Erreur lors du traitement de la requête: {}", requestUri, e);
throw e;
}
}
/**
* Effectue les vérifications de sécurité nécessaires.
*
* @param request La requête HTTP à vérifier
* @return true si la requête passe les vérifications de sécurité
*/
private boolean isSecurityCheckPassed(HttpServletRequest request) {
// Implémentez ici votre logique de sécurité spécifique
// Par exemple : vérification des tokens, authentification, autorisations...
log.trace("Vérification de sécurité pour: {}", request.getRequestURI());
return true; // À adapter selon vos besoins de sécurité
}
/**
* Méthode appelée lors de la destruction du filtre.
*/
@Override
public void destroy() {
log.info("Destruction du filtre de sécurité");
}
package dev.lions.security;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* Filtre de sécurité pour l'application.
* Implémente la logique de sécurité pour toutes les requêtes entrantes.
*
* @author Lions Dev Team
* @version 1.0
*/
@Slf4j
@ApplicationScoped
@WebFilter(urlPatterns = "/*")
public class SecurityFilter implements Filter {
/**
* Initialise le filtre.
* Cette méthode est appelée par le conteneur lors du démarrage.
*
* @param filterConfig Configuration du filtre
*/
@Override
public void init(FilterConfig filterConfig) {
log.info("Initialisation du filtre de sécurité");
}
/**
* Applique la logique de filtrage sur chaque requête.
*
* @param request La requête entrante
* @param response La réponse
* @param chain La chaîne de filtres
* @throws IOException En cas d'erreur d'entrée/sortie
* @throws ServletException En cas d'erreur de servlet
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestUri = httpRequest.getRequestURI();
log.debug("Traitement de la requête: {}", requestUri);
try {
// Vérification de sécurité de base
if (isSecurityCheckPassed(httpRequest)) {
chain.doFilter(request, response);
} else {
log.warn("Accès refusé pour la requête: {}", requestUri);
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Accès refusé");
}
} catch (Exception e) {
log.error("Erreur lors du traitement de la requête: {}", requestUri, e);
throw e;
}
}
/**
* Effectue les vérifications de sécurité nécessaires.
*
* @param request La requête HTTP à vérifier
* @return true si la requête passe les vérifications de sécurité
*/
private boolean isSecurityCheckPassed(HttpServletRequest request) {
// Implémentez ici votre logique de sécurité spécifique
// Par exemple : vérification des tokens, authentification, autorisations...
log.trace("Vérification de sécurité pour: {}", request.getRequestURI());
return true; // À adapter selon vos besoins de sécurité
}
/**
* Méthode appelée lors de la destruction du filtre.
*/
@Override
public void destroy() {
log.info("Destruction du filtre de sécurité");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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