Compare commits
5 Commits
51265fb0fa
...
9e23db1728
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e23db1728 | |||
| 9a41b4ca17 | |||
| 106e8f7c88 | |||
| ac4146132b | |||
|
|
f0959abd75 |
76
.gitea/workflows/ci.yml
Normal file
76
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
# ============================================================================
|
||||
# Template — .gitea/workflows/ci.yml
|
||||
# Drop this file into each app repo (adjust LIONS_JAVA_VERSION +
|
||||
# LIONS_APP_NAME + optional --deploy-repo-url). It runs inside the
|
||||
# registry.lions.dev/lionsdev/lionsctl-ci:latest image, so lionsctl,
|
||||
# kubectl, helm, docker CLI, JDK 17+21 and Maven are all pre-installed.
|
||||
#
|
||||
# Required Gitea repo secrets:
|
||||
# LIONS_REGISTRY_USERNAME (typically "lionsregistry")
|
||||
# LIONS_REGISTRY_PASSWORD
|
||||
# LIONS_GIT_USERNAME (typically "lionsdev")
|
||||
# LIONS_GIT_ACCESS_TOKEN (Gitea PAT with write:repository, write:package)
|
||||
# LIONS_GIT_PASSWORD (Gitea password for same user — Helm mode)
|
||||
# SMTP_HOST SMTP_PORT SMTP_USERNAME SMTP_PASSWORD SMTP_FROM
|
||||
# ============================================================================
|
||||
name: CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch: {}
|
||||
|
||||
env:
|
||||
# Adjust per repo:
|
||||
# - unionflow-server-impl-quarkus -> 21
|
||||
# - all others -> 17
|
||||
LIONS_JAVA_VERSION: "17"
|
||||
LIONS_CLUSTER: "k1"
|
||||
|
||||
jobs:
|
||||
pipeline:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: registry.lions.dev/lionsdev/lionsctl-ci:latest
|
||||
credentials:
|
||||
username: ${{ secrets.LIONS_REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.LIONS_REGISTRY_PASSWORD }}
|
||||
# Mount the host docker socket so `docker build/push` inside the
|
||||
# container hits the runner's daemon (DinD-free).
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
steps:
|
||||
- name: Show tooling
|
||||
run: |
|
||||
lionsctl --version || true
|
||||
docker --version
|
||||
kubectl version --client=true
|
||||
helm version --short
|
||||
mvn --version | head -n2
|
||||
|
||||
- name: Pipeline deploy
|
||||
env:
|
||||
LIONS_REGISTRY_USERNAME: ${{ secrets.LIONS_REGISTRY_USERNAME }}
|
||||
LIONS_REGISTRY_PASSWORD: ${{ secrets.LIONS_REGISTRY_PASSWORD }}
|
||||
LIONS_GIT_USERNAME: ${{ secrets.LIONS_GIT_USERNAME }}
|
||||
LIONS_GIT_ACCESS_TOKEN: ${{ secrets.LIONS_GIT_ACCESS_TOKEN }}
|
||||
LIONS_GIT_PASSWORD: ${{ secrets.LIONS_GIT_PASSWORD }}
|
||||
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||
SMTP_PORT: ${{ secrets.SMTP_PORT }}
|
||||
SMTP_USERNAME: ${{ secrets.SMTP_USERNAME }}
|
||||
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||
SMTP_FROM: ${{ secrets.SMTP_FROM }}
|
||||
# No actions/checkout — lionsctl clones internally using git_access_token.
|
||||
run: |
|
||||
# For btpxpress-backend add: --deploy-repo-url https://git.lions.dev/lionsdev/btpxpress-server-k1
|
||||
# For btpxpress-frontend add: --deploy-repo-url https://git.lions.dev/lionsdev/btpxpress-client-k1
|
||||
lionsctl pipeline \
|
||||
-u ${{ gitea.server_url }}/${{ gitea.repository }} \
|
||||
-b ${{ gitea.ref_name }} \
|
||||
-j ${{ env.LIONS_JAVA_VERSION }} \
|
||||
-e production \
|
||||
-c ${{ env.LIONS_CLUSTER }} \
|
||||
-p prod \
|
||||
--deploy-repo-url https://git.lions.dev/lionsdev/lionsdev-client-impl-quarkus-k1 \
|
||||
-m admin@lions.dev
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal 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
34
pom.xml
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
393
src/main/java/dev/lions/audit/AuditDataInitializer.java
Normal file
393
src/main/java/dev/lions/audit/AuditDataInitializer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
86
src/main/java/dev/lions/audit/AuditQuestion.java
Normal file
86
src/main/java/dev/lions/audit/AuditQuestion.java
Normal 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; }
|
||||
}
|
||||
305
src/main/java/dev/lions/audit/AuditReportService.java
Normal file
305
src/main/java/dev/lions/audit/AuditReportService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
198
src/main/java/dev/lions/audit/AuditResource.java
Normal file
198
src/main/java/dev/lions/audit/AuditResource.java
Normal 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+_.-]+@(.+)$");
|
||||
}
|
||||
}
|
||||
145
src/main/java/dev/lions/audit/AuditResponse.java
Normal file
145
src/main/java/dev/lions/audit/AuditResponse.java
Normal 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; }
|
||||
}
|
||||
247
src/main/java/dev/lions/audit/AuditService.java
Normal file
247
src/main/java/dev/lions/audit/AuditService.java
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
129
src/main/java/dev/lions/audit/AuditSubmissionDTO.java
Normal file
129
src/main/java/dev/lions/audit/AuditSubmissionDTO.java
Normal 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; }
|
||||
}
|
||||
534
src/main/java/dev/lions/compliance/IvorianTaxService.java
Normal file
534
src/main/java/dev/lions/compliance/IvorianTaxService.java
Normal 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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package dev.lions.components;
|
||||
|
||||
public class DataTableView {
|
||||
}
|
||||
package dev.lions.components;
|
||||
|
||||
public class DataTableView {
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
197
src/main/java/dev/lions/erp/core/Company.java
Normal file
197
src/main/java/dev/lions/erp/core/Company.java
Normal 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
|
||||
}
|
||||
338
src/main/java/dev/lions/erp/core/TenantService.java
Normal file
338
src/main/java/dev/lions/erp/core/TenantService.java
Normal 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; }
|
||||
}
|
||||
257
src/main/java/dev/lions/erp/core/User.java
Normal file
257
src/main/java/dev/lions/erp/core/User.java
Normal 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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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("-+", "-");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
11
src/main/java/dev/lions/quote/ComplexityLevel.java
Normal file
11
src/main/java/dev/lions/quote/ComplexityLevel.java
Normal 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%)
|
||||
}
|
||||
158
src/main/java/dev/lions/quote/ModuleCatalog.java
Normal file
158
src/main/java/dev/lions/quote/ModuleCatalog.java
Normal 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; }
|
||||
}
|
||||
245
src/main/java/dev/lions/quote/Quote.java
Normal file
245
src/main/java/dev/lions/quote/Quote.java
Normal 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é
|
||||
}
|
||||
42
src/main/java/dev/lions/quote/QuoteCustomizationDTO.java
Normal file
42
src/main/java/dev/lions/quote/QuoteCustomizationDTO.java
Normal 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; }
|
||||
}
|
||||
181
src/main/java/dev/lions/quote/QuoteDTO.java
Normal file
181
src/main/java/dev/lions/quote/QuoteDTO.java
Normal 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; }
|
||||
}
|
||||
|
||||
|
||||
115
src/main/java/dev/lions/quote/QuoteModule.java
Normal file
115
src/main/java/dev/lions/quote/QuoteModule.java
Normal 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; }
|
||||
}
|
||||
|
||||
|
||||
70
src/main/java/dev/lions/quote/QuoteModuleDTO.java
Normal file
70
src/main/java/dev/lions/quote/QuoteModuleDTO.java
Normal 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; }
|
||||
}
|
||||
421
src/main/java/dev/lions/quote/QuoteReportService.java
Normal file
421
src/main/java/dev/lions/quote/QuoteReportService.java
Normal 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";
|
||||
}
|
||||
}
|
||||
296
src/main/java/dev/lions/quote/QuoteResource.java
Normal file
296
src/main/java/dev/lions/quote/QuoteResource.java
Normal 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();
|
||||
}
|
||||
}
|
||||
356
src/main/java/dev/lions/quote/QuoteService.java
Normal file
356
src/main/java/dev/lions/quote/QuoteService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
256
src/main/java/dev/lions/roi/ROICalculator.java
Normal file
256
src/main/java/dev/lions/roi/ROICalculator.java
Normal 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; }
|
||||
}
|
||||
170
src/main/java/dev/lions/roi/ROIResource.java
Normal file
170
src/main/java/dev/lions/roi/ROIResource.java
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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é");
|
||||
}
|
||||
}
|
||||
@@ -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é");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user