Compare commits

..

4 Commits

Author SHA1 Message Date
David
d0f0de67ca feature ci
Some checks failed
CI/CD Pipeline for Spring Boot / build-and-test (push) Failing after 5m48s
CI/CD Pipeline for Spring Boot / docker (push) Has been skipped
CI/CD Pipeline for Spring Boot / Deploy - Docker - serveur (push) Has been skipped
2025-09-12 12:01:20 +02:00
David
428e656598 Merge branch 'dev'
Some checks failed
CI/CD Pipeline for Spring Boot / docker (push) Blocked by required conditions
CI/CD Pipeline for Spring Boot / Deploy - Docker - serveur (push) Blocked by required conditions
CI/CD Pipeline for Spring Boot / build-and-test (push) Has been cancelled
2025-09-12 12:00:08 +02:00
David
b473c4cbe5 feature ci
Some checks failed
CI/CD Pipeline for Spring Boot Dev / docker (push) Has been cancelled
CI/CD Pipeline for Spring Boot Dev / Deploy - Docker - serveur (push) Has been cancelled
CI/CD Pipeline for Spring Boot Dev / build-and-test (push) Has been cancelled
2025-09-12 11:59:48 +02:00
David
1e0b50bd86 feature ci 2025-09-12 11:59:35 +02:00
124 changed files with 766 additions and 8590 deletions

View File

@ -1,166 +0,0 @@
# ===========================================
# XPEDITIS Backend Environment Configuration
# Copy this file to .env and fill in your values
# ===========================================
# ===========================================
# SERVER CONFIGURATION
# ===========================================
SERVER_PORT=8080
# ===========================================
# DATABASE CONFIGURATION
# ===========================================
# Development Database (H2) - No changes needed
SPRING_H2_CONSOLE_ENABLED=false
SPRING_H2_DATASOURCE_URL=jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
SPRING_H2_DATASOURCE_DRIVER_CLASS_NAME=org.h2.Driver
SPRING_H2_DATASOURCE_USERNAME=sa
SPRING_H2_DATASOURCE_PASSWORD=
# Production Database (MySQL) - FILL IN YOUR VALUES
SPRING_DATASOURCE_URL=jdbc:mysql://your-mysql-host:3306/xpeditis?useSSL=false&serverTimezone=UTC
SPRING_DATASOURCE_USERNAME=your_mysql_username
SPRING_DATASOURCE_PASSWORD=your_mysql_password
SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver
# ===========================================
# JPA/HIBERNATE CONFIGURATION
# ===========================================
SPRING_JPA_SHOW_SQL=false
SPRING_JPA_FORMAT_SQL=false
SPRING_JPA_DATABASE_PLATFORM_H2=org.hibernate.dialect.H2Dialect
SPRING_JPA_DATABASE_PLATFORM_MYSQL=org.hibernate.dialect.MySQLDialect
# Development
SPRING_JPA_HIBERNATE_DDL_AUTO_DEV=create-drop
SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_DEV=true
# Production
SPRING_JPA_HIBERNATE_DDL_AUTO_PROD=validate
SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_PROD=false
# ===========================================
# FLYWAY CONFIGURATION
# ===========================================
# Development
SPRING_FLYWAY_ENABLED_DEV=false
# Production
SPRING_FLYWAY_ENABLED_PROD=true
SPRING_FLYWAY_LOCATIONS=classpath:db/migration/structure,classpath:db/migration/data
SPRING_FLYWAY_VALIDATE_ON_MIGRATE=true
SPRING_FLYWAY_BASELINE_ON_MIGRATE=true
SPRING_FLYWAY_BASELINE_VERSION=0
SPRING_FLYWAY_DEFAULT_SCHEMA=leblr
# ===========================================
# EMAIL CONFIGURATION
# ===========================================
# Development (Mailtrap) - FILL IN YOUR MAILTRAP CREDENTIALS
SPRING_MAIL_HOST_DEV=sandbox.smtp.mailtrap.io
SPRING_MAIL_PORT_DEV=2525
SPRING_MAIL_USERNAME_DEV=your_mailtrap_username
SPRING_MAIL_PASSWORD_DEV=your_mailtrap_password
APPLICATION_EMAIL_FROM_DEV=noreply@xpeditis.local
# Production (OVH or your SMTP provider) - FILL IN YOUR VALUES
SPRING_MAIL_PROTOCOL_PROD=smtp
SPRING_MAIL_HOST_PROD=your-smtp-host
SPRING_MAIL_PORT_PROD=587
SPRING_MAIL_USERNAME_PROD=your-email@domain.com
SPRING_MAIL_PASSWORD_PROD=your_email_password
APPLICATION_EMAIL_FROM_PROD=your-email@domain.com
# Email Properties
SPRING_MAIL_SMTP_AUTH=true
SPRING_MAIL_SMTP_STARTTLS_ENABLE=true
SPRING_MAIL_SMTP_SSL_TRUST=*
SPRING_MAIL_SMTP_CONNECTION_TIMEOUT=5000
SPRING_MAIL_SMTP_TIMEOUT=3000
SPRING_MAIL_SMTP_WRITE_TIMEOUT=5000
# ===========================================
# OAUTH2 / GOOGLE CONFIGURATION
# ===========================================
# FILL IN YOUR GOOGLE OAUTH2 CREDENTIALS
GOOGLE_CLIENT_ID=your-google-client-id-from-console
GOOGLE_CLIENT_SECRET=your-google-client-secret-from-console
# Development
OAUTH2_REDIRECT_URI_DEV=http://localhost:8080/login/oauth2/code/google
# Production - FILL IN YOUR DOMAIN
OAUTH2_REDIRECT_URI_PROD=https://your-domain.com/login/oauth2/code/google
# Google OAuth2 URLs (standard - don't change)
GOOGLE_AUTHORIZATION_URI=https://accounts.google.com/o/oauth2/v2/auth
GOOGLE_TOKEN_URI=https://oauth2.googleapis.com/token
GOOGLE_USER_INFO_URI=https://www.googleapis.com/oauth2/v2/userinfo
GOOGLE_USER_NAME_ATTRIBUTE=sub
GOOGLE_OAUTH2_SCOPE=openid,email,profile
# ===========================================
# SECURITY / JWT CONFIGURATION
# ===========================================
# GENERATE A SECURE SECRET KEY FOR PRODUCTION
JWT_SECRET_KEY=your-very-secure-jwt-secret-key-here-make-it-long-and-random
JWT_EXPIRATION=86400000
JWT_REFRESH_TOKEN_EXPIRATION=604800000
# CSRF Configuration
APPLICATION_CSRF_ENABLED_DEV=false
APPLICATION_CSRF_ENABLED_PROD=true
# ===========================================
# APPLICATION SPECIFIC CONFIGURATION
# ===========================================
# OAuth2 Settings
APPLICATION_OAUTH2_GOOGLE_ENABLED=true
# License Configuration
APPLICATION_LICENSE_TRIAL_DURATION_DAYS=30
APPLICATION_LICENSE_TRIAL_MAX_USERS=5
APPLICATION_LICENSE_BASIC_MAX_USERS=50
APPLICATION_LICENSE_PREMIUM_MAX_USERS=200
APPLICATION_LICENSE_ENTERPRISE_MAX_USERS=1000
# ===========================================
# FILE UPLOAD CONFIGURATION
# ===========================================
FILE_UPLOAD_DIR=/upload
SPRING_SERVLET_MULTIPART_ENABLED=true
SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE=50MB
SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE=50MB
# ===========================================
# APPLICATION METADATA
# ===========================================
SPRING_APPLICATION_NAME=XPEDITIS Backend
SPRING_APPLICATION_VERSION=0.0.1-SNAPSHOT
# ===========================================
# ACTIVE PROFILES
# ===========================================
# Use 'dev' for development, 'prod' for production
SPRING_PROFILES_ACTIVE=dev
# ===========================================
# LOGGING CONFIGURATION
# ===========================================
LOGGING_LEVEL_ROOT=INFO
LOGGING_LEVEL_SPRINGFRAMEWORK_BOOT_AUTOCONFIGURE=OFF
LOGGING_LEVEL_COMMONS_REQUEST_LOGGING_FILTER=INFO
LOGGING_LEVEL_HIBERNATE_SQL=OFF
LOGGING_LEVEL_HIBERNATE_TYPE=OFF
# ===========================================
# DEVELOPMENT ONLY
# ===========================================
# Uncomment for development debugging
# SPRING_JPA_SHOW_SQL=true
# SPRING_JPA_FORMAT_SQL=true
# LOGGING_LEVEL_ROOT=DEBUG

105
.gitea/workflows/ci.yml Executable file
View File

@ -0,0 +1,105 @@
name: CI/CD Pipeline for Spring Boot
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
# Step 1: Checkout code
- name: Checkout Code
uses: actions/checkout@v3
# Step 2: Set up JDK
- name: Set up JDK 23
uses: actions/setup-java@v3
with:
java-version: 23
distribution: 'temurin'
# Step 3: Cache Maven dependencies
- name: Cache Maven Dependencies
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
# Step 4: Build and test
- name: Build and Test
run: |
./mvnw clean verify
docker:
runs-on: ubuntu-latest
needs: [ build-and-test ]
steps:
- name: Install Docker
run: |
apt-get update
apt-get install -y docker.io
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Download buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -sL https://github.com/docker/buildx/releases/download/v0.11.2/buildx-v0.11.2.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod +x ~/.docker/cli-plugins/docker-buildx
- name: Setup buildx
run: |
docker buildx create --use
docker buildx inspect --bootstrap
- name: Login to Cloud Coding Registry
uses: docker/login-action@v2
with:
registry: rg.fr-par.scw.cloud/weworkstudio
username: nologin
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@master
with:
context: .
file: ./Dockerfile
push: true
tags: rg.fr-par.scw.cloud/weworkstudio/xpeditis-backend:prod
build-args: |
XPEDITIS_PROFILE=prod
- name: Cleanup buildx
run: |
docker buildx rm
- name: Docker cleanup
run: docker system prune -af
- name: Uninstall Docker
run: |
apt-get purge -y docker.io
apt-get autoremove -y --purge docker.io
rm -rf /var/lib/docker /etc/docker
deploy_server:
name: Deploy - Docker - serveur
runs-on: ubuntu-latest
needs: [ docker ]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Déclencher le Webhook
run: |
curl -X POST -H "Content-Type:application/json" -d '{"data": "example" }' ${{ secrets.WEBHOOK_URL }}

94
.gitea/workflows/dev.yml Normal file
View File

@ -0,0 +1,94 @@
name: CI/CD Pipeline for Spring Boot Dev
on:
push:
branches:
- dev
pull_request:
branches:
- dev
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
# Step 1: Checkout code
- name: Checkout Code
uses: actions/checkout@v3
# Step 2: Set up JDK
- name: Set up JDK 23
uses: actions/setup-java@v3
with:
java-version: 23
distribution: 'temurin'
# Step 3: Cache Maven dependencies
- name: Cache Maven Dependencies
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
# Step 4: Build and test
- name: Build and Test
run: |
./mvnw clean verify
docker:
runs-on: ubuntu-latest
needs: [ build-and-test ]
steps:
- name: Install Docker
run: |
apt-get update
apt-get install -y docker.io
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Download buildx
run: |
mkdir -p ~/.docker/cli-plugins
curl -sL https://github.com/docker/buildx/releases/download/v0.11.2/buildx-v0.11.2.linux-amd64 -o ~/.docker/cli-plugins/docker-buildx
chmod +x ~/.docker/cli-plugins/docker-buildx
- name: Setup buildx
run: |
docker buildx create --use
docker buildx inspect --bootstrap
- name: Login to Cloud Coding Registry
uses: docker/login-action@v2
with:
registry: rg.fr-par.scw.cloud/weworkstudio
username: nologin
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@master
with:
context: .
file: ./Dockerfile
push: true
tags: rg.fr-par.scw.cloud/weworkstudio/xpeditis-backend:dev
build-args: |
XPEDITIS_PROFILE=dev
- name: Cleanup buildx
run: |
docker buildx rm
- name: Docker cleanup
run: docker system prune -af
- name: Uninstall Docker
run: |
apt-get purge -y docker.io
apt-get autoremove -y --purge docker.io
rm -rf /var/lib/docker /etc/docker

300
CLAUDE.md
View File

@ -1,300 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Xpeditis is a Spring Boot-based logistics/shipping application built with a modular Maven architecture. The project follows Hexagonal Architecture (Clean Architecture) principles with clear separation of concerns.
## Architecture
### Modular Structure
The project is organized into Maven modules:
- **bootstrap**: Main application entry point and configuration (XpeditisApplication.java)
- **application**: REST controllers and web layer (AuthenticationRestController, UserRestController)
- **domain**: Core business logic split into:
- **api**: Service interfaces (UserService, AuthenticationService, etc.)
- **data**: DTOs and exceptions (UserAccount, AuthenticationRequest/Response)
- **service**: Service implementations (UserServiceImpl, AuthenticationServiceImpl)
- **spi**: Repository interfaces (UserRepository, AuthenticationRepository)
- **infrastructure**: Data access layer with JPA entities, repositories, and external integrations
- **common**: Shared utilities (CommonUtil)
### Key Technologies
- Spring Boot 3.4.1 with Java 23
- Spring Security with JWT authentication
- JPA/Hibernate for data persistence
- MapStruct for entity mapping
- Lombok for boilerplate reduction
- Flyway for database migrations
- MySQL (prod) / H2 (dev) databases
## Development Commands
### Build and Test
```bash
# Clean build and install all modules
./mvnw clean install
# Run tests
./mvnw test
# Run single test class
./mvnw test -Dtest=UserServiceImplTest
# Run specific test method
./mvnw test -Dtest=UserServiceImplTest#shouldCreateUser
```
### Database Management
```bash
# Development (H2) - migrate database
./mvnw clean install flyway:migrate '-Dflyway.configFiles=flyway-h2.conf' -Pdev
# Production (MySQL) - migrate database
./mvnw clean install flyway:migrate -Pprod
# Start application with dev profile (default)
./mvnw spring-boot:run
# Start with production profile
./mvnw spring-boot:run -Pprod
```
### Docker Operations
```bash
# Build and start services with Docker Compose
docker-compose up --build
# Start database only
docker-compose up db
# View logs
docker-compose logs -f back
```
## Development Profiles
- **dev** (default): Uses H2 in-memory database, simplified configuration
- **prod**: Uses MySQL database, includes Flyway migrations, production-ready settings
## Key Configuration Files
- `bootstrap/src/main/resources/application.yml`: Main configuration
- `bootstrap/src/main/resources/application-dev.yml`: Development overrides
- `bootstrap/src/main/resources/application-prod.yml`: Production overrides
- `infrastructure/flyway.conf`: Database migration configuration
- `compose.yml`: Docker services configuration
## API Documentation
Swagger UI is available at: http://localhost:8080/swagger-ui.html
## Security Implementation
The application uses JWT-based authentication with:
- JWT token generation and validation (JwtUtil)
- Security configuration (SecurityConfiguration)
- Authentication filter (JwtAuthenticationFilter)
- Logout service (LogoutService)
- Role-based access control with Permission enum
## Testing Strategy
- Unit tests for service layer implementations (AddressServiceImplTest, etc.)
- Repository tests for data access layer (AddressJpaRepositoryTest, etc.)
- Integration tests in bootstrap module (LeBlrApplicationTests)
- Use AssertJ for fluent assertions
## Database Schema
Database migrations are in `infrastructure/src/main/resources/db/migration/`:
- `structure/V1__INIT_DB_SCHEMA.sql`: Initial schema
- `data/V1.1__INIT_DB_DATA.sql`: Initial data
## Logging
Structured logging configuration in `logback-spring.xml` with:
- Production logs written to `./logs/prod/`
- Request logging via CommonsRequestLoggingFilter
- Configurable log levels per package
## 🏗️ Checklist Architecture Hexagonale - Étapes de Validation Pré-Codage
### 📋 Phase 1 : Analyse et Compréhension du Contexte
#### ✅ 1.1 Identification du Domaine Métier
- [ ] Clarifier le domaine d'activité : Quel est le métier principal ?
- [ ] Identifier les entités métier principales : Quels sont les objets centraux ?
- [ ] Définir les règles de gestion : Quelles sont les contraintes business ?
- [ ] Mapper les cas d'usage : Que doit faire l'application ?
#### ✅ 1.2 Analyse des Besoins d'Intégration
- [ ] Identifier les acteurs externes : Qui utilise l'application ?
- [ ] Répertorier les services externes : BDD, APIs, systèmes de fichiers ?
- [ ] Définir les interfaces d'entrée : REST, GraphQL, CLI ?
- [ ] Spécifier les interfaces de sortie : Persistance, notifications, etc.
### 📐 Phase 2 : Conception Architecturale
#### ✅ 2.1 Structure des Modules Maven
- [ ] Valider la structure 4 modules :
- `domain` (cœur hexagonal)
- `application` (couche application)
- `infrastructure` (couche infrastructure)
- `bootstrap` (module de lancement)
#### ✅ 2.2 Définition des Ports
- [ ] Identifier les Ports API (interfaces exposées par le domaine) :
- Services métier que le domaine expose
- Interfaces appelées par les adaptateurs d'entrée
- [ ] Identifier les Ports SPI (interfaces requises par le domaine) :
- Repositories pour la persistance
- Services externes (notifications, intégrations)
- Interfaces que le domaine définit mais n'implémente pas
#### ✅ 2.3 Conception des Adaptateurs
- [ ] Adaptateurs d'Entrée (Driving) :
- Contrôleurs REST
- Interfaces utilisateur
- Tests automatisés
- [ ] Adaptateurs de Sortie (Driven) :
- Implémentations des repositories
- Clients pour services externes
- Systèmes de fichiers/messaging
### 🏛️ Phase 3 : Architecture en Couches
#### ✅ 3.1 Module Domain (Cœur)
- [ ] Vérifier l'isolation complète : Aucune dépendance externe
- [ ] Structure du package :
```
com.{project}.domain/
├── model/ # Entités métier
├── service/ # Services du domaine
├── port/
│ ├── in/ # Ports d'entrée (API)
│ └── out/ # Ports de sortie (SPI)
└── exception/ # Exceptions métier
```
#### ✅ 3.2 Module Application
- [ ] Responsabilités claires : Exposition des services
- [ ] Structure du package :
```
com.{project}.application/
├── controller/ # Contrôleurs REST
├── dto/ # Data Transfer Objects
├── mapper/ # Mappers DTO ↔ Domain
└── config/ # Configuration Spring
```
#### ✅ 3.3 Module Infrastructure
- [ ] Implémentation des SPI : Tous les ports de sortie
- [ ] Structure du package :
```
com.{project}.infrastructure/
├── adapter/
│ ├── in/ # Adaptateurs d'entrée (si applicable)
│ └── out/ # Adaptateurs de sortie
├── repository/ # Implémentations JPA
├── entity/ # Entités JPA
└── mapper/ # Mappers JPA ↔ Domain
```
#### ✅ 3.4 Module bootstrap
- [ ] Point d'entrée unique : Classe @SpringBootApplication
- [ ] Assemblage des dépendances : Configuration complète
- [ ] Dépendances vers tous les modules
### 🔧 Phase 4 : Validation Technique
#### ✅ 4.1 Gestion des Dépendances
- [ ] Module domain : Aucune dépendance externe (sauf tests)
- [ ] Module application : Dépend uniquement de domain
- [ ] Module infrastructure : Dépend uniquement de domain
- [ ] Module bootstrap : Dépend de tous les autres modules
#### ✅ 4.2 Respect des Patterns
- [ ] Domain-Driven Design : Entités, Value Objects, Aggregates
- [ ] SOLID Principles : SRP, DIP, ISP appliqués
- [ ] Repository Pattern : Abstraction de la persistance
- [ ] Clean Architecture : Règle de dépendance respectée
#### ✅ 4.3 Configuration Spring
- [ ] Injection de dépendance : Utilisation de @Configuration
- [ ] Éviter @Component dans le domaine
- [ ] Gestion des transactions : @Transactional dans l'application
### 🧪 Phase 5 : Stratégie de Test
#### ✅ 5.1 Tests par Couche
- [ ] Tests unitaires du domaine : Sans dépendances externes
- [ ] Tests d'intégration : Par adaptateur spécifique
- [ ] Tests end-to-end : Flux complets
- [ ] Mocking strategy : Interfaces bien définies
#### ✅ 5.2 Testabilité
- [ ] Isolation du domaine : Tests sans Spring Context
- [ ] Adaptateurs mockables : Interfaces clairement définies
- [ ] Données de test : Jeux de données cohérents
### 📝 Phase 6 : Naming et Conventions
#### ✅ 6.1 Conventions de Nommage
- [ ] Ports : Suffixe "Port" ou noms explicites d'interfaces
- [ ] Adaptateurs : Suffixe "Adapter"
- [ ] Services : Suffixe "Service"
- [ ] Repositories : Suffixe "Repository"
#### ✅ 6.2 Structure des Packages
- [ ] Cohérence : Nommage uniforme entre modules
- [ ] Lisibilité : Structure claire et intuitive
- [ ] Séparation : Concerns bien séparés
### 🎯 Phase 7 : Questions de Validation Finale
#### ✅ 7.1 Questions Critiques à Se Poser
- [ ] Le domaine est-il complètement isolé ?
- [ ] Les dépendances pointent-elles vers l'intérieur ?
- [ ] Peut-on tester le domaine sans Spring ?
- [ ] Peut-on changer de base de données sans impact sur le domaine ?
- [ ] Peut-on ajouter une nouvelle interface (GraphQL) facilement ?
#### ✅ 7.2 Validation des Flux
- [ ] Flux d'entrée : HTTP → Contrôleur → Service → Port API
- [ ] Flux de sortie : Port SPI → Adaptateur → Base/Service externe
- [ ] Mapping : DTO ↔ Domain ↔ JPA entities clairement défini
### 🚦 Phase 8 : Checklist Pré-Codage
#### ✅ 8.1 Avant de Commencer
- [ ] Architecture validée avec le demandeur
- [ ] Modules Maven structure approuvée
- [ ] Ports et Adaptateurs identifiés et documentés
- [ ] Stack technique confirmée (Spring Boot, H2, MapStruct)
- [ ] Stratégie de tests définie
#### ✅ 8.2 Ordre de Développement Recommandé
1. Module domain : Entités, services, ports
2. Module infrastructure : Adaptateurs de sortie, repositories
3. Module application : Contrôleurs, DTOs, mappers
4. Module bootstrap : Configuration, assemblage
5. Tests : Unitaires → Intégration → E2E
### 💡 Points d'Attention Spéciaux
#### ⚠️ Pièges à Éviter
- Dépendances circulaires entre modules
- Logique métier dans les adaptateurs
- Annotations Spring dans le domaine
- Couplage fort entre couches
- Tests qui nécessitent le contexte Spring pour le domaine
#### 🎯 Objectifs à Atteindre
- Domaine pur et testable isolément
- Flexibilité pour changer d'adaptateurs
- Maintenabilité et évolutivité
- Séparation claire des responsabilités
- Code réutilisable et modulaire

View File

@ -1,155 +0,0 @@
# Environment Configuration Guide
## Overview
Ce projet utilise des variables d'environnement pour la configuration. Toutes les variables ont été centralisées dans des fichiers `.env`.
## Setup Instructions
### 1. Copy the example file
```bash
cp .env.example .env
```
### 2. Edit the `.env` file with your values
#### Required for Development:
- `GOOGLE_CLIENT_ID` - Votre Google OAuth2 Client ID
- `GOOGLE_CLIENT_SECRET` - Votre Google OAuth2 Client Secret
- `JWT_SECRET_KEY` - Clé secrète pour JWT (générez une clé sécurisée)
#### Required for Production:
- Database credentials (`SPRING_DATASOURCE_*`)
- Email configuration (`SPRING_MAIL_*`)
- Production OAuth2 redirect URI
- Secure JWT secret key
## Environment Profiles
### Development (`SPRING_PROFILES_ACTIVE=dev`)
- Uses H2 in-memory database
- Uses Mailtrap for email testing
- CSRF disabled
- Flyway disabled
### Production (`SPRING_PROFILES_ACTIVE=prod`)
- Uses MySQL database
- Uses production SMTP server
- CSRF enabled
- Flyway enabled for migrations
## Key Variables by Category
### 🗄️ Database
```bash
# Development (H2)
SPRING_H2_DATASOURCE_URL=jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# Production (MySQL)
SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/xpeditis
SPRING_DATASOURCE_USERNAME=your_username
SPRING_DATASOURCE_PASSWORD=your_password
```
### 📧 Email
```bash
# Development (Mailtrap)
SPRING_MAIL_HOST_DEV=sandbox.smtp.mailtrap.io
SPRING_MAIL_USERNAME_DEV=your_mailtrap_username
SPRING_MAIL_PASSWORD_DEV=your_mailtrap_password
# Production
SPRING_MAIL_HOST_PROD=your-smtp-host
SPRING_MAIL_USERNAME_PROD=your-email@domain.com
SPRING_MAIL_PASSWORD_PROD=your_password
```
### 🔐 Security & OAuth2
```bash
# JWT
JWT_SECRET_KEY=your-secure-secret-key
JWT_EXPIRATION=86400000
# Google OAuth2
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
```
### 📋 License Limits
```bash
APPLICATION_LICENSE_TRIAL_MAX_USERS=5
APPLICATION_LICENSE_BASIC_MAX_USERS=50
APPLICATION_LICENSE_PREMIUM_MAX_USERS=200
APPLICATION_LICENSE_ENTERPRISE_MAX_USERS=1000
```
## Security Notes
### 🛡️ Important Security Practices:
1. **Never commit `.env` files** to version control
2. **Generate secure JWT secret keys** for production
3. **Use environment-specific secrets**
4. **Rotate keys regularly** in production
### 🔑 JWT Secret Key Generation:
```bash
# Generate a secure 256-bit key
openssl rand -hex 32
# Or use online generator (ensure HTTPS):
# https://generate-secret.vercel.app/32
```
## Usage in Application
### 🎯 Easy Startup Scripts
Use the provided scripts for easy development and production startup:
```bash
# Development mode (loads .env automatically)
./run-dev.sh
# Production mode (validates required variables)
./run-prod.sh
# Or traditional way
./mvnw spring-boot:run
```
### Spring Boot Configuration Loading Order:
1. System environment variables (highest priority)
2. `.env` file variables
3. `application-{profile}.yml` files
4. `application.yml` (lowest priority)
### 🔧 All YAML files now support .env variables:
- `application.yml` - Base configuration with .env support
- `application-dev.yml` - Development overrides with .env support
- `application-prod.yml` - Production overrides with .env support
## Docker Support
For Docker deployments, mount the `.env` file:
```bash
docker run -d \
--env-file .env \
-p 8080:8080 \
xpeditis-backend
```
## Troubleshooting
### Common Issues:
1. **Missing variables**: Check `.env.example` for required variables
2. **Database connection**: Verify database credentials and host
3. **Email not sending**: Check SMTP configuration
4. **OAuth2 not working**: Verify Google Console settings and redirect URIs
### Debugging:
```bash
# Enable debug logging
LOGGING_LEVEL_ROOT=DEBUG
# Show SQL queries
SPRING_JPA_SHOW_SQL=true
SPRING_JPA_FORMAT_SQL=true
```

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# leblr-backend
Le BLR backend side<br>
SWAGGER UI : http://localhost:8080/swagger-ui.html<br>
<br>
.\mvnw clean install flyway:migrate -Pprod<br>
.\mvnw clean install flyway:migrate '-Dflyway.configFiles=flyway-h2.conf' -Pdev<br>

View File

@ -1,540 +0,0 @@
{
"info": {
"name": "Xpeditis API Collection",
"description": "Collection complète des endpoints pour l'API Xpeditis - Plateforme de shipping maritime et logistique",
"version": "1.0.0",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{jwt_token}}",
"type": "string"
}
]
},
"variable": [
{
"key": "base_url",
"value": "http://localhost:8080",
"type": "string"
},
{
"key": "jwt_token",
"value": "",
"type": "string"
},
{
"key": "refresh_token",
"value": "",
"type": "string"
}
],
"item": [
{
"name": "🏠 Service Info",
"item": [
{
"name": "Get Service Information",
"request": {
"method": "GET",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"url": {
"raw": "{{base_url}}/",
"host": ["{{base_url}}"],
"path": [""]
}
},
"response": []
}
],
"description": "Endpoints pour récupérer les informations du service"
},
{
"name": "🔐 Authentication",
"item": [
{
"name": "Login",
"event": [
{
"listen": "test",
"script": {
"exec": [
"if (pm.response.code === 200) {",
" var jsonData = pm.response.json();",
" pm.collectionVariables.set('jwt_token', jsonData.accessToken);",
" pm.collectionVariables.set('refresh_token', jsonData.refreshToken);",
"}"
]
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"username\": \"user@example.com\",\n \"password\": \"Password123!\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/auth/login",
"host": ["{{base_url}}"],
"path": ["api", "v1", "auth", "login"]
}
},
"response": []
},
{
"name": "Register",
"event": [
{
"listen": "test",
"script": {
"exec": [
"if (pm.response.code === 201) {",
" var jsonData = pm.response.json();",
" pm.collectionVariables.set('jwt_token', jsonData.accessToken);",
" pm.collectionVariables.set('refresh_token', jsonData.refreshToken);",
"}"
]
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john.doe@example.com\",\n \"username\": \"johndoe\",\n \"password\": \"Password123!\",\n \"confirmPassword\": \"Password123!\",\n \"phoneNumber\": \"+33123456789\",\n \"companyName\": \"Maritime Solutions Inc\",\n \"companyCountry\": \"France\",\n \"authProvider\": \"LOCAL\",\n \"privacyPolicyAccepted\": true\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/auth/register",
"host": ["{{base_url}}"],
"path": ["api", "v1", "auth", "register"]
}
},
"response": []
},
{
"name": "Google OAuth",
"event": [
{
"listen": "test",
"script": {
"exec": [
"if (pm.response.code === 200) {",
" var jsonData = pm.response.json();",
" pm.collectionVariables.set('jwt_token', jsonData.accessToken);",
" pm.collectionVariables.set('refresh_token', jsonData.refreshToken);",
"}"
]
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"googleToken\": \"google_oauth_token_here\",\n \"companyName\": \"Optional Company Name\",\n \"phoneNumber\": \"+33123456789\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/auth/google",
"host": ["{{base_url}}"],
"path": ["api", "v1", "auth", "google"]
}
},
"response": []
},
{
"name": "Refresh Token",
"event": [
{
"listen": "test",
"script": {
"exec": [
"if (pm.response.code === 200) {",
" var jsonData = pm.response.json();",
" pm.collectionVariables.set('jwt_token', jsonData.accessToken);",
" pm.collectionVariables.set('refresh_token', jsonData.refreshToken);",
"}"
]
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"refreshToken\": \"{{refresh_token}}\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/auth/refresh",
"host": ["{{base_url}}"],
"path": ["api", "v1", "auth", "refresh"]
}
},
"response": []
},
{
"name": "Logout",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{jwt_token}}"
}
],
"url": {
"raw": "{{base_url}}/api/v1/auth/logout",
"host": ["{{base_url}}"],
"path": ["api", "v1", "auth", "logout"]
}
},
"response": []
}
],
"description": "Endpoints d'authentification et gestion des sessions JWT"
},
{
"name": "👤 Profile Management",
"item": [
{
"name": "Get Profile",
"request": {
"method": "GET",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{jwt_token}}"
}
],
"url": {
"raw": "{{base_url}}/api/v1/profile",
"host": ["{{base_url}}"],
"path": ["api", "v1", "profile"]
}
},
"response": []
},
{
"name": "Update Profile",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{jwt_token}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"firstName\": \"John Updated\",\n \"lastName\": \"Doe Updated\",\n \"phoneNumber\": \"+33987654321\",\n \"username\": \"johnupdated\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/profile",
"host": ["{{base_url}}"],
"path": ["api", "v1", "profile"]
}
},
"response": []
}
],
"description": "Endpoints de gestion du profil utilisateur via ProfileController"
},
{
"name": "👥 User Management",
"item": [
{
"name": "Get User Profile (Alternative)",
"request": {
"method": "GET",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{jwt_token}}"
}
],
"url": {
"raw": "{{base_url}}/api/v1/users/profile",
"host": ["{{base_url}}"],
"path": ["api", "v1", "users", "profile"]
}
},
"response": []
},
{
"name": "Update User Profile (Alternative)",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{jwt_token}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"firstName\": \"John Updated\",\n \"lastName\": \"Doe Updated\",\n \"phoneNumber\": \"+33987654321\",\n \"username\": \"johnupdated\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/users/profile",
"host": ["{{base_url}}"],
"path": ["api", "v1", "users", "profile"]
}
},
"response": []
},
{
"name": "Change Password",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{jwt_token}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"currentPassword\": \"Password123!\",\n \"newPassword\": \"NewPassword123!\",\n \"confirmationPassword\": \"NewPassword123!\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/users/password",
"host": ["{{base_url}}"],
"path": ["api", "v1", "users", "password"]
}
},
"response": []
},
{
"name": "Delete Account",
"request": {
"method": "DELETE",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{jwt_token}}"
}
],
"url": {
"raw": "{{base_url}}/api/v1/users/account",
"host": ["{{base_url}}"],
"path": ["api", "v1", "users", "account"]
}
},
"response": []
},
{
"name": "List Users (Admin Only)",
"request": {
"method": "GET",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{jwt_token}}"
}
],
"url": {
"raw": "{{base_url}}/api/v1/users?page=0&size=10",
"host": ["{{base_url}}"],
"path": ["api", "v1", "users"],
"query": [
{
"key": "page",
"value": "0"
},
{
"key": "size",
"value": "10"
},
{
"key": "companyId",
"value": "uuid-company-id",
"disabled": true
}
]
}
},
"response": []
}
],
"description": "Endpoints de gestion des utilisateurs - profil, mot de passe, suppression de compte"
},
{
"name": "📋 Devis Transport",
"item": [
{
"name": "Calculer Devis (3 offres automatiques)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"typeService\": \"EXPORT\",\n \"incoterm\": \"FOB\",\n \"typeLivraison\": \"PORTE_A_PORTE\",\n \"depart\": {\n \"ville\": \"Marseille\",\n \"codePostal\": \"13000\",\n \"pays\": \"France\",\n \"coordonneesGps\": \"43.2965,5.3698\"\n },\n \"arrivee\": {\n \"ville\": \"Shanghai\",\n \"codePostal\": \"200000\",\n \"pays\": \"Chine\",\n \"coordonneesGps\": \"31.2304,121.4737\"\n },\n \"douaneImportExport\": \"EXPORT\",\n \"eur1Import\": false,\n \"eur1Export\": true,\n \"colisages\": [\n {\n \"type\": \"PALETTE\",\n \"quantite\": 3,\n \"longueur\": 120.0,\n \"largeur\": 80.0,\n \"hauteur\": 160.0,\n \"poids\": 750.5,\n \"gerbable\": true\n },\n {\n \"type\": \"CAISSE\",\n \"quantite\": 2,\n \"longueur\": 100.0,\n \"largeur\": 60.0,\n \"hauteur\": 120.0,\n \"poids\": 450.2,\n \"gerbable\": false\n }\n ],\n \"marchandiseDangereuse\": {\n \"presente\": true,\n \"classe\": \"3\",\n \"numeroOnu\": \"UN1263\",\n \"description\": \"Peintures inflammables\"\n },\n \"manutentionParticuliere\": {\n \"hayon\": true,\n \"sangles\": true,\n \"couvertureThermique\": false,\n \"autres\": \"Manutention précautionneuse requise\"\n },\n \"produitsReglementes\": {\n \"alimentaire\": false,\n \"pharmaceutique\": false,\n \"autres\": null\n },\n \"servicesAdditionnels\": {\n \"rendezVousLivraison\": true,\n \"documentT1\": false,\n \"stopDouane\": true,\n \"assistanceExport\": true,\n \"assurance\": true,\n \"valeurDeclaree\": 15000.0\n },\n \"dateEnlevement\": \"2025-01-15\",\n \"dateLivraison\": \"2025-02-20\",\n \"nomClient\": \"Maritime Solutions SAS\",\n \"emailClient\": \"contact@maritime-solutions.fr\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/devis/calculer",
"host": ["{{base_url}}"],
"path": ["api", "v1", "devis", "calculer"]
}
},
"response": []
},
{
"name": "Valider Demande de Devis",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"typeService\": \"EXPORT\",\n \"incoterm\": \"FOB\",\n \"depart\": {\n \"ville\": \"Marseille\",\n \"codePostal\": \"13000\",\n \"pays\": \"France\"\n },\n \"arrivee\": {\n \"ville\": \"Shanghai\",\n \"codePostal\": \"200000\",\n \"pays\": \"Chine\"\n },\n \"colisages\": [\n {\n \"type\": \"CAISSE\",\n \"quantite\": 2,\n \"poids\": 150.5,\n \"longueur\": 120.0,\n \"largeur\": 80.0,\n \"hauteur\": 100.0,\n \"gerbable\": false\n }\n ],\n \"marchandiseDangereuse\": null,\n \"nomClient\": \"Test Client\",\n \"emailClient\": \"test@test.com\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/devis/valider",
"host": ["{{base_url}}"],
"path": ["api", "v1", "devis", "valider"]
}
},
"response": []
},
{
"name": "Calculer Devis - Exemple Simple",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"typeService\": \"EXPORT\",\n \"incoterm\": \"FOB\",\n \"depart\": {\n \"ville\": \"Marseille\",\n \"codePostal\": \"13000\",\n \"pays\": \"France\"\n },\n \"arrivee\": {\n \"ville\": \"Shanghai\",\n \"codePostal\": \"200000\",\n \"pays\": \"Chine\"\n },\n \"colisages\": [\n {\n \"type\": \"CAISSE\",\n \"quantite\": 2,\n \"poids\": 150.5,\n \"longueur\": 120.0,\n \"largeur\": 80.0,\n \"hauteur\": 100.0,\n \"gerbable\": false\n }\n ],\n \"marchandiseDangereuse\": null,\n \"nomClient\": \"Test Client\",\n \"emailClient\": \"test@test.com\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/devis/calculer",
"host": ["{{base_url}}"],
"path": ["api", "v1", "devis", "calculer"]
}
},
"response": []
},
{
"name": "Calculer Devis - Avec Marchandises Dangereuses",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"typeService\": \"EXPORT\",\n \"incoterm\": \"FOB\",\n \"depart\": {\n \"ville\": \"Marseille\",\n \"codePostal\": \"13000\",\n \"pays\": \"France\"\n },\n \"arrivee\": {\n \"ville\": \"Hong Kong\",\n \"codePostal\": \"999077\",\n \"pays\": \"Hong Kong\"\n },\n \"colisages\": [\n {\n \"type\": \"PALETTE\",\n \"quantite\": 5,\n \"poids\": 890.0,\n \"longueur\": 120.0,\n \"largeur\": 80.0,\n \"hauteur\": 200.0,\n \"gerbable\": true\n }\n ],\n \"marchandiseDangereuse\": {\n \"presente\": true,\n \"classe\": \"3\",\n \"numeroOnu\": \"UN1263\",\n \"description\": \"Produits chimiques inflammables\"\n },\n \"servicesAdditionnels\": {\n \"assurance\": true,\n \"valeurDeclaree\": 25000.0,\n \"rendezVousLivraison\": true,\n \"assistanceExport\": true\n },\n \"nomClient\": \"Chemical Industries Ltd\",\n \"emailClient\": \"export@chemical-industries.com\"\n}"
},
"url": {
"raw": "{{base_url}}/api/v1/devis/calculer",
"host": ["{{base_url}}"],
"path": ["api", "v1", "devis", "calculer"]
}
},
"response": []
}
],
"description": "API de calcul automatisé de devis transport maritime - génère 3 offres (Rapide, Standard, Économique) basées sur les grilles tarifaires LESCHACO"
}
],
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
]
}

View File

@ -29,14 +29,6 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>

View File

@ -2,101 +2,35 @@ package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.AuthenticationService; import com.dh7789dev.xpeditis.AuthenticationService;
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest; import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
import com.dh7789dev.xpeditis.dto.request.GoogleAuthRequest;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse; import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
import com.dh7789dev.xpeditis.dto.response.UserResponse; import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Slf4j
@RestController @RestController
@RequestMapping(value = "${apiPrefix}/api/v1/auth", produces = APPLICATION_JSON_VALUE) @RequestMapping(value = "${apiPrefix}/api/v1/auth",
produces = APPLICATION_JSON_VALUE)
@RequiredArgsConstructor @RequiredArgsConstructor
@Tag(name = "Authentication", description = "Authentication and registration endpoints")
public class AuthenticationRestController { public class AuthenticationRestController {
private final AuthenticationService authenticationService; private final AuthenticationService service;
@Operation(summary = "User login", description = "Authenticate user with email/username and password")
@ApiResponse(responseCode = "200", description = "Successfully authenticated")
@ApiResponse(responseCode = "401", description = "Invalid credentials")
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<AuthenticationResponse> authenticate( public ResponseEntity<AuthenticationResponse> authenticate(
@RequestBody @Valid AuthenticationRequest request) { @RequestBody @Valid AuthenticationRequest request) {
log.info("Authentication attempt for user: {}", request.getUsername()); return ResponseEntity.ok(service.authenticate(request));
AuthenticationResponse response = authenticationService.authenticate(request);
return ResponseEntity.ok(response);
} }
@Operation(summary = "User registration", description = "Register a new user account")
@ApiResponse(responseCode = "201", description = "User successfully registered")
@ApiResponse(responseCode = "400", description = "Invalid registration data")
@ApiResponse(responseCode = "409", description = "User already exists")
@PostMapping("/register") @PostMapping("/register")
public ResponseEntity<AuthenticationResponse> register( public ResponseEntity<AuthenticationResponse> register(
@RequestBody @Valid RegisterRequest request) { @RequestBody @Valid RegisterRequest request) {
log.info("Registration attempt for email: {}", request.getEmail()); return ResponseEntity.ok(service.register(request));
AuthenticationResponse response = authenticationService.register(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@Operation(summary = "Google OAuth authentication", description = "Authenticate user with Google OAuth token")
@ApiResponse(responseCode = "200", description = "Successfully authenticated with Google")
@ApiResponse(responseCode = "401", description = "Invalid Google token")
@PostMapping("/google")
public ResponseEntity<AuthenticationResponse> authenticateWithGoogle(
@RequestBody @Valid GoogleAuthRequest request) {
log.info("Google OAuth authentication attempt");
AuthenticationResponse response = authenticationService.authenticateWithGoogle(request.getGoogleToken());
return ResponseEntity.ok(response);
}
@Operation(summary = "Refresh token", description = "Get a new access token using refresh token")
@ApiResponse(responseCode = "200", description = "Token successfully refreshed")
@ApiResponse(responseCode = "401", description = "Invalid refresh token")
@PostMapping("/refresh")
public ResponseEntity<AuthenticationResponse> refreshToken(
@RequestBody RefreshTokenRequest request) {
log.info("Token refresh attempt");
AuthenticationResponse response = authenticationService.refreshToken(request.getRefreshToken());
return ResponseEntity.ok(response);
}
@Operation(summary = "User logout", description = "Invalidate user session and tokens")
@ApiResponse(responseCode = "204", description = "Successfully logged out")
@PostMapping("/logout")
public ResponseEntity<Void> logout(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
authenticationService.logout(token);
}
return ResponseEntity.noContent().build();
}
// Inner class for refresh token request
public static class RefreshTokenRequest {
@Valid
private String refreshToken;
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
} }
} }

View File

@ -1,107 +0,0 @@
package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.DevisCalculService;
import com.dh7789dev.xpeditis.dto.app.DemandeDevis;
import com.dh7789dev.xpeditis.dto.app.ReponseDevis;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/v1/devis")
@RequiredArgsConstructor
@Validated
@Slf4j
@Tag(name = "Devis", description = "API pour le calcul automatisé de devis transport")
public class DevisRestController {
private final DevisCalculService devisCalculService;
@Operation(
summary = "Calculer les 3 offres de transport",
description = "Génère automatiquement 3 offres (Rapide, Standard, Économique) basées sur les grilles tarifaires"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "Devis calculé avec succès",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ReponseDevis.class))
),
@ApiResponse(
responseCode = "400",
description = "Données de la demande invalides"
),
@ApiResponse(
responseCode = "404",
description = "Aucune grille tarifaire applicable"
),
@ApiResponse(
responseCode = "500",
description = "Erreur interne du serveur"
)
})
@PostMapping("/calculer")
public ResponseEntity<ReponseDevis> calculerDevis(
@Parameter(description = "Demande de devis avec tous les détails du transport", required = true)
@Valid @RequestBody DemandeDevis demandeDevis) {
log.info("Demande de calcul de devis reçue pour le client: {}", demandeDevis.getNomClient());
try {
ReponseDevis reponseDevis = devisCalculService.calculerTroisOffres(demandeDevis);
log.info("Devis calculé avec succès - {} offres générées", reponseDevis.getOffres().size());
return ResponseEntity.ok(reponseDevis);
} catch (IllegalArgumentException e) {
log.warn("Données de demande invalides: {}", e.getMessage());
return ResponseEntity.badRequest().build();
} catch (IllegalStateException e) {
log.warn("Aucune grille tarifaire applicable: {}", e.getMessage());
return ResponseEntity.notFound().build();
} catch (Exception e) {
log.error("Erreur lors du calcul du devis", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@Operation(
summary = "Valider une demande de devis",
description = "Vérifie que tous les champs obligatoires sont présents et valides"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Demande de devis valide"),
@ApiResponse(responseCode = "400", description = "Demande de devis invalide")
})
@PostMapping("/valider")
public ResponseEntity<String> validerDemandeDevis(
@Parameter(description = "Demande de devis à valider", required = true)
@Valid @RequestBody DemandeDevis demandeDevis) {
log.debug("Validation de la demande de devis");
try {
devisCalculService.validerDemandeDevis(demandeDevis);
return ResponseEntity.ok("Demande de devis valide");
} catch (IllegalArgumentException e) {
log.warn("Demande de devis invalide: {}", e.getMessage());
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}

View File

@ -1,358 +0,0 @@
package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.GrilleTarifaireService;
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.Valid;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/grilles-tarifaires")
@RequiredArgsConstructor
@Validated
@Slf4j
@Tag(name = "Grilles Tarifaires", description = "API pour la gestion et l'import des grilles tarifaires")
public class GrilleTarifaireRestController {
private final GrilleTarifaireService grilleTarifaireService;
@Operation(
summary = "Importer des grilles tarifaires depuis un fichier CSV",
description = "Import en masse de grilles tarifaires au format CSV avec validation des données"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "Import réussi avec détails des grilles importées"
),
@ApiResponse(
responseCode = "400",
description = "Fichier invalide ou erreurs de validation"
),
@ApiResponse(
responseCode = "415",
description = "Format de fichier non supporté"
)
})
@PostMapping("/import/csv")
public ResponseEntity<?> importerDepuisCsv(
@Parameter(description = "Fichier CSV contenant les grilles tarifaires", required = true)
@RequestParam("file") MultipartFile file,
@Parameter(description = "Mode d'import: REPLACE (remplace tout) ou MERGE (fusionne)")
@RequestParam(defaultValue = "MERGE") String mode) {
log.info("Début d'import CSV - Fichier: {}, Taille: {} bytes, Mode: {}",
file.getOriginalFilename(), file.getSize(), mode);
try {
if (file.isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("erreur", "Le fichier ne peut pas être vide"));
}
if (!file.getOriginalFilename().toLowerCase().endsWith(".csv")) {
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
.body(Map.of("erreur", "Seuls les fichiers CSV sont supportés"));
}
List<GrilleTarifaire> grillesImportees = grilleTarifaireService.importerDepuisCsv(file, mode);
log.info("Import CSV terminé avec succès - {} grilles importées", grillesImportees.size());
return ResponseEntity.ok(Map.of(
"message", "Import réussi",
"nombreGrillesImportees", grillesImportees.size(),
"grilles", grillesImportees
));
} catch (IOException e) {
log.error("Erreur lors de la lecture du fichier CSV", e);
return ResponseEntity.badRequest()
.body(Map.of("erreur", "Erreur lors de la lecture du fichier: " + e.getMessage()));
} catch (IllegalArgumentException e) {
log.warn("Données CSV invalides: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(Map.of("erreur", "Données invalides: " + e.getMessage()));
} catch (Exception e) {
log.error("Erreur lors de l'import CSV", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("erreur", "Erreur interne lors de l'import"));
}
}
@Operation(
summary = "Importer des grilles tarifaires depuis un fichier Excel",
description = "Import en masse de grilles tarifaires au format Excel (.xlsx) avec validation"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Import réussi"),
@ApiResponse(responseCode = "400", description = "Fichier invalide"),
@ApiResponse(responseCode = "415", description = "Format de fichier non supporté")
})
@PostMapping("/import/excel")
public ResponseEntity<?> importerDepuisExcel(
@Parameter(description = "Fichier Excel (.xlsx) contenant les grilles tarifaires", required = true)
@RequestParam("file") MultipartFile file,
@Parameter(description = "Nom de la feuille à importer (optionnel)")
@RequestParam(required = false) String sheetName,
@Parameter(description = "Mode d'import: REPLACE ou MERGE")
@RequestParam(defaultValue = "MERGE") String mode) {
log.info("Début d'import Excel - Fichier: {}, Feuille: {}, Mode: {}",
file.getOriginalFilename(), sheetName, mode);
try {
if (file.isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("erreur", "Le fichier ne peut pas être vide"));
}
String filename = file.getOriginalFilename().toLowerCase();
if (!filename.endsWith(".xlsx") && !filename.endsWith(".xls")) {
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
.body(Map.of("erreur", "Seuls les fichiers Excel (.xlsx, .xls) sont supportés"));
}
List<GrilleTarifaire> grillesImportees = grilleTarifaireService.importerDepuisExcel(file, sheetName, mode);
log.info("Import Excel terminé avec succès - {} grilles importées", grillesImportees.size());
return ResponseEntity.ok(Map.of(
"message", "Import réussi",
"nombreGrillesImportees", grillesImportees.size(),
"grilles", grillesImportees
));
} catch (IOException e) {
log.error("Erreur lors de la lecture du fichier Excel", e);
return ResponseEntity.badRequest()
.body(Map.of("erreur", "Erreur lors de la lecture du fichier: " + e.getMessage()));
} catch (Exception e) {
log.error("Erreur lors de l'import Excel", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("erreur", "Erreur interne lors de l'import"));
}
}
@Operation(
summary = "Importer grilles tarifaires depuis JSON",
description = "Import de grilles tarifaires au format JSON avec validation complète"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Import réussi"),
@ApiResponse(responseCode = "400", description = "JSON invalide ou erreurs de validation")
})
@PostMapping("/import/json")
public ResponseEntity<?> importerDepuisJson(
@Parameter(description = "Liste des grilles tarifaires au format JSON", required = true)
@Valid @RequestBody List<GrilleTarifaire> grilles,
@Parameter(description = "Mode d'import: REPLACE ou MERGE")
@RequestParam(defaultValue = "MERGE") String mode) {
log.info("Début d'import JSON - {} grilles à traiter, Mode: {}", grilles.size(), mode);
try {
List<GrilleTarifaire> grillesImportees = grilleTarifaireService.importerDepuisJson(grilles, mode);
log.info("Import JSON terminé avec succès - {} grilles importées", grillesImportees.size());
return ResponseEntity.ok(Map.of(
"message", "Import réussi",
"nombreGrillesImportees", grillesImportees.size(),
"grilles", grillesImportees
));
} catch (IllegalArgumentException e) {
log.warn("Données JSON invalides: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(Map.of("erreur", "Données invalides: " + e.getMessage()));
} catch (Exception e) {
log.error("Erreur lors de l'import JSON", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("erreur", "Erreur interne lors de l'import"));
}
}
@Operation(
summary = "Créer ou mettre à jour une grille tarifaire",
description = "Crée une nouvelle grille ou met à jour une grille existante"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "Grille créée avec succès"),
@ApiResponse(responseCode = "200", description = "Grille mise à jour avec succès"),
@ApiResponse(responseCode = "400", description = "Données invalides")
})
@PostMapping
public ResponseEntity<?> creerOuMettreAJourGrille(
@Parameter(description = "Grille tarifaire à créer ou mettre à jour", required = true)
@Valid @RequestBody GrilleTarifaire grille) {
log.info("Création/mise à jour grille tarifaire - Transporteur: {}, Route: {} -> {}",
grille.getTransporteur(), grille.getOriginePays(), grille.getDestinationPays());
try {
boolean isNew = (grille.getId() == null);
GrilleTarifaire grilleSauvegardee = grilleTarifaireService.sauvegarderGrille(grille);
HttpStatus status = isNew ? HttpStatus.CREATED : HttpStatus.OK;
String message = isNew ? "Grille créée avec succès" : "Grille mise à jour avec succès";
log.info("{} - ID: {}", message, grilleSauvegardee.getId());
return ResponseEntity.status(status).body(Map.of(
"message", message,
"grille", grilleSauvegardee
));
} catch (IllegalArgumentException e) {
log.warn("Données de grille invalides: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(Map.of("erreur", "Données invalides: " + e.getMessage()));
} catch (Exception e) {
log.error("Erreur lors de la sauvegarde de la grille", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("erreur", "Erreur interne lors de la sauvegarde"));
}
}
@Operation(
summary = "Lister toutes les grilles tarifaires",
description = "Récupère la liste complète des grilles tarifaires avec pagination"
)
@GetMapping
public ResponseEntity<?> listerGrilles(
@Parameter(description = "Numéro de page (0-based)")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "Taille de la page")
@RequestParam(defaultValue = "20") int size,
@Parameter(description = "Filtrer par transporteur")
@RequestParam(required = false) String transporteur,
@Parameter(description = "Filtrer par pays d'origine")
@RequestParam(required = false) String paysOrigine,
@Parameter(description = "Filtrer par pays de destination")
@RequestParam(required = false) String paysDestination) {
try {
List<GrilleTarifaire> grilles = grilleTarifaireService.listerGrilles(
page, size, transporteur, paysOrigine, paysDestination);
return ResponseEntity.ok(Map.of(
"grilles", grilles,
"page", page,
"size", size,
"total", grilles.size()
));
} catch (Exception e) {
log.error("Erreur lors de la récupération des grilles", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("erreur", "Erreur lors de la récupération des grilles"));
}
}
@Operation(
summary = "Récupérer une grille tarifaire par ID",
description = "Récupère le détail complet d'une grille tarifaire"
)
@GetMapping("/{id}")
public ResponseEntity<?> obtenirGrilleParId(
@Parameter(description = "Identifiant de la grille tarifaire", required = true)
@PathVariable Long id) {
try {
GrilleTarifaire grille = grilleTarifaireService.trouverParId(id);
if (grille == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(grille);
} catch (Exception e) {
log.error("Erreur lors de la récupération de la grille ID: {}", id, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("erreur", "Erreur lors de la récupération de la grille"));
}
}
@Operation(
summary = "Supprimer une grille tarifaire",
description = "Supprime définitivement une grille tarifaire"
)
@DeleteMapping("/{id}")
public ResponseEntity<?> supprimerGrille(
@Parameter(description = "Identifiant de la grille tarifaire à supprimer", required = true)
@PathVariable Long id) {
try {
grilleTarifaireService.supprimerGrille(id);
log.info("Grille tarifaire supprimée - ID: {}", id);
return ResponseEntity.ok(Map.of(
"message", "Grille supprimée avec succès",
"id", id
));
} catch (Exception e) {
log.error("Erreur lors de la suppression de la grille ID: {}", id, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("erreur", "Erreur lors de la suppression de la grille"));
}
}
@Operation(
summary = "Valider la structure d'un fichier d'import",
description = "Valide la structure et les données d'un fichier avant import effectif"
)
@PostMapping("/validate")
public ResponseEntity<?> validerFichier(
@Parameter(description = "Fichier à valider (CSV ou Excel)", required = true)
@RequestParam("file") MultipartFile file) {
log.info("Validation fichier - Nom: {}, Taille: {} bytes",
file.getOriginalFilename(), file.getSize());
try {
if (file.isEmpty()) {
return ResponseEntity.badRequest()
.body(Map.of("erreur", "Le fichier ne peut pas être vide"));
}
Map<String, Object> resultatsValidation = grilleTarifaireService.validerFichier(file);
return ResponseEntity.ok(resultatsValidation);
} catch (IOException e) {
log.error("Erreur lors de la lecture du fichier", e);
return ResponseEntity.badRequest()
.body(Map.of("erreur", "Erreur lors de la lecture du fichier: " + e.getMessage()));
} catch (Exception e) {
log.error("Erreur lors de la validation", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("erreur", "Erreur lors de la validation"));
}
}
}

View File

@ -1,103 +0,0 @@
package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.UserService;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.UpdateProfileRequest;
import com.dh7789dev.xpeditis.dto.response.UserResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "${apiPrefix}/api/v1/profile", produces = APPLICATION_JSON_VALUE)
@Tag(name = "Profile", description = "User profile management endpoints")
public class ProfileController {
private final UserService userService;
@Operation(summary = "Get user profile", description = "Retrieve the current user's profile information")
@ApiResponse(responseCode = "200", description = "Profile retrieved successfully")
@ApiResponse(responseCode = "401", description = "Unauthorized")
@GetMapping
public ResponseEntity<UserResponse> getProfile(Authentication authentication) {
log.info("Profile retrieval request for user: {}", authentication.getName());
UserAccount userAccount = userService.findByUsername(authentication.getName())
.orElseThrow(() -> new RuntimeException("User not found"));
UserResponse userResponse = mapToUserResponse(userAccount);
return ResponseEntity.ok(userResponse);
}
@Operation(summary = "Update profile", description = "Update the current user's profile information")
@ApiResponse(responseCode = "200", description = "Profile updated successfully")
@ApiResponse(responseCode = "400", description = "Invalid profile data")
@ApiResponse(responseCode = "401", description = "Unauthorized")
@PutMapping
public ResponseEntity<UserResponse> updateProfile(
@RequestBody @Valid UpdateProfileRequest request,
Authentication authentication) {
log.info("Profile update request for user: {}", authentication.getName());
// Get current user
UserAccount currentUser = userService.findByUsername(authentication.getName())
.orElseThrow(() -> new RuntimeException("User not found"));
// Update fields if provided
if (request.getFirstName() != null && !request.getFirstName().trim().isEmpty()) {
currentUser.setFirstName(request.getFirstName().trim());
}
if (request.getLastName() != null && !request.getLastName().trim().isEmpty()) {
currentUser.setLastName(request.getLastName().trim());
}
if (request.getPhoneNumber() != null && !request.getPhoneNumber().trim().isEmpty()) {
currentUser.setPhoneNumber(new com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber(request.getPhoneNumber()));
}
if (request.getUsername() != null && !request.getUsername().trim().isEmpty()) {
// Check if username is available
if (!userService.existsByUsername(request.getUsername()) ||
request.getUsername().equals(currentUser.getUsername())) {
currentUser.setUsername(request.getUsername().trim());
} else {
throw new RuntimeException("Username already exists");
}
}
// Save updated user
UserAccount updatedUser = userService.updateProfile(currentUser);
UserResponse userResponse = mapToUserResponse(updatedUser);
return ResponseEntity.ok(userResponse);
}
// Helper method to convert UserAccount to UserResponse
private UserResponse mapToUserResponse(UserAccount userAccount) {
return UserResponse.builder()
.id(userAccount.getId())
.firstName(userAccount.getFirstName())
.lastName(userAccount.getLastName())
.email(userAccount.getEmail() != null ? userAccount.getEmail().getValue() : null)
.username(userAccount.getUsername())
.phoneNumber(userAccount.getPhoneNumber() != null ? userAccount.getPhoneNumber().getValue() : null)
.authProvider(userAccount.getAuthProvider())
.privacyPolicyAccepted(userAccount.isPrivacyPolicyAccepted())
.privacyPolicyAcceptedAt(userAccount.getPrivacyPolicyAcceptedAt())
.createdAt(userAccount.getCreatedAt())
.lastLoginAt(userAccount.getLastLoginAt())
.isActive(userAccount.isActive())
.role(userAccount.getRole() != null ? userAccount.getRole().name() : null)
.companyName(userAccount.getCompany() != null ? userAccount.getCompany().getName() : null)
.companyId(userAccount.getCompany() != null ? userAccount.getCompany().getId() : null)
.build();
}
}

View File

@ -1,153 +1,38 @@
package com.dh7789dev.xpeditis.controller.api.v1; package com.dh7789dev.xpeditis.controller.api.v1;
import com.dh7789dev.xpeditis.UserService; import com.dh7789dev.xpeditis.UserService;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest; import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.dto.request.UpdateProfileRequest;
import com.dh7789dev.xpeditis.dto.response.UserResponse;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.security.Principal; import java.security.Principal;
import java.util.UUID;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@Slf4j
@RestController @RestController
@Validated @Validated
@RequiredArgsConstructor @RequestMapping(value = "${apiPrefix}/api/v1/users",
@RequestMapping(value = "${apiPrefix}/api/v1/users", produces = APPLICATION_JSON_VALUE) produces = APPLICATION_JSON_VALUE)
@Tag(name = "User Management", description = "User profile and management endpoints")
public class UserRestController { public class UserRestController {
private final UserService userService; private final UserService service;
@Operation(summary = "Get current user profile", description = "Retrieve the profile of the authenticated user") public UserRestController(UserService service) {
@ApiResponse(responseCode = "200", description = "User profile retrieved successfully") this.service = service;
@GetMapping("/profile")
public ResponseEntity<UserResponse> getProfile(Authentication authentication) {
log.info("Profile request for user: {}", authentication.getName());
// Get user by username from authentication
UserAccount userAccount = userService.findByUsername(authentication.getName())
.orElseThrow(() -> new RuntimeException("User not found"));
// Convert to response DTO
UserResponse userResponse = mapToUserResponse(userAccount);
return ResponseEntity.ok(userResponse);
} }
@Operation(summary = "Update user profile", description = "Update profile information of the authenticated user") @Operation(summary = "Change password of the connected user")
@ApiResponse(responseCode = "200", description = "Profile updated successfully") @PatchMapping("/password")
@ApiResponse(responseCode = "400", description = "Invalid profile data")
@PutMapping("/profile")
public ResponseEntity<UserResponse> updateProfile(
@RequestBody @Valid UpdateProfileRequest request,
Authentication authentication) {
log.info("Profile update request for user: {}", authentication.getName());
// Get current user
UserAccount currentUser = userService.findByUsername(authentication.getName())
.orElseThrow(() -> new RuntimeException("User not found"));
// Update fields
if (request.getFirstName() != null) {
currentUser.setFirstName(request.getFirstName());
}
if (request.getLastName() != null) {
currentUser.setLastName(request.getLastName());
}
if (request.getPhoneNumber() != null) {
currentUser.setPhoneNumber(new com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber(request.getPhoneNumber()));
}
if (request.getUsername() != null && !userService.existsByUsername(request.getUsername())) {
currentUser.setUsername(request.getUsername());
}
// Save updated user
UserAccount updatedUser = userService.updateProfile(currentUser);
UserResponse userResponse = mapToUserResponse(updatedUser);
return ResponseEntity.ok(userResponse);
}
@Operation(summary = "Change password", description = "Change password of the authenticated user")
@ApiResponse(responseCode = "204", description = "Password changed successfully")
@ApiResponse(responseCode = "400", description = "Invalid password data")
@PutMapping("/password")
public ResponseEntity<Void> changePassword( public ResponseEntity<Void> changePassword(
@RequestBody @Valid ChangePasswordRequest request, @RequestBody ChangePasswordRequest request,
Principal connectedUser) { Principal connectedUser) {
log.info("Password change request for user: {}", connectedUser.getName()); service.changePassword(request, connectedUser);
userService.changePassword(request, connectedUser); return new ResponseEntity<>(HttpStatus.OK);
return ResponseEntity.noContent().build();
}
@Operation(summary = "Delete user account", description = "Delete the authenticated user's account")
@ApiResponse(responseCode = "204", description = "Account deleted successfully")
@DeleteMapping("/account")
public ResponseEntity<Void> deleteAccount(Authentication authentication) {
log.info("Account deletion request for user: {}", authentication.getName());
UserAccount user = userService.findByUsername(authentication.getName())
.orElseThrow(() -> new RuntimeException("User not found"));
userService.deleteUser(user.getId());
return ResponseEntity.noContent().build();
}
@Operation(summary = "List users", description = "List all users (Admin only)")
@ApiResponse(responseCode = "200", description = "Users retrieved successfully")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Page<UserResponse>> listUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) UUID companyId) {
log.info("User list request - page: {}, size: {}, companyId: {}", page, size, companyId);
Pageable pageable = PageRequest.of(page, size);
// TODO: Implement pagination and company filtering in service layer
// For now, return an empty page
Page<UserResponse> users = Page.empty(pageable);
return ResponseEntity.ok(users);
}
// Helper method to convert UserAccount to UserResponse
private UserResponse mapToUserResponse(UserAccount userAccount) {
return UserResponse.builder()
.id(userAccount.getId())
.firstName(userAccount.getFirstName())
.lastName(userAccount.getLastName())
.email(userAccount.getEmail() != null ? userAccount.getEmail().getValue() : null)
.username(userAccount.getUsername())
.phoneNumber(userAccount.getPhoneNumber() != null ? userAccount.getPhoneNumber().getValue() : null)
.authProvider(userAccount.getAuthProvider())
.privacyPolicyAccepted(userAccount.isPrivacyPolicyAccepted())
.privacyPolicyAcceptedAt(userAccount.getPrivacyPolicyAcceptedAt())
.createdAt(userAccount.getCreatedAt())
.lastLoginAt(userAccount.getLastLoginAt())
.isActive(userAccount.isActive())
.role(userAccount.getRole() != null ? userAccount.getRole().name() : null)
.companyName(userAccount.getCompany() != null ? userAccount.getCompany().getName() : null)
.companyId(userAccount.getCompany() != null ? userAccount.getCompany().getId() : null)
.build();
} }
} }

View File

@ -10,7 +10,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@ComponentScan(basePackages = {"com.dh7789dev.xpeditis"}) @ComponentScan(basePackages = {"com.dh7789dev.xpeditis"})
@EnableJpaRepositories({"com.dh7789dev.xpeditis.dao", "com.dh7789dev.xpeditis.repository"}) @EnableJpaRepositories("com.dh7789dev.xpeditis.dao")
@EntityScan("com.dh7789dev.xpeditis.entity") @EntityScan("com.dh7789dev.xpeditis.entity")
@EnableJpaAuditing(auditorAwareRef = "auditorProvider") @EnableJpaAuditing(auditorAwareRef = "auditorProvider")
@SpringBootApplication @SpringBootApplication

View File

@ -1,7 +1,6 @@
package com.dh7789dev.xpeditis.configuration; package com.dh7789dev.xpeditis.configuration;
import com.dh7789dev.xpeditis.dao.UserDao; import com.dh7789dev.xpeditis.dao.UserDao;
import com.dh7789dev.xpeditis.entity.UserEntity;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@ -1,19 +0,0 @@
package com.dh7789dev.xpeditis.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import lombok.Data;
@Configuration
@ConfigurationProperties(prefix = "spring.security.oauth2.client.registration.google")
@Data
public class OAuth2Configuration {
private String clientId;
private String clientSecret;
private String redirectUri;
private String scope;
public static final String GOOGLE_TOKEN_INFO_URL = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=%s";
public static final String GOOGLE_USER_INFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=%s";
}

View File

@ -1,14 +0,0 @@
package com.dh7789dev.xpeditis.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@ -11,7 +11,6 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@ -28,7 +27,7 @@ import static org.springframework.security.web.util.matcher.AntPathRequestMatche
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) //@EnableMethodSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
@Value("${application.csrf.enabled}") @Value("${application.csrf.enabled}")
@ -41,18 +40,8 @@ public class SecurityConfiguration {
private static final String[] WHITE_LIST_URL = { private static final String[] WHITE_LIST_URL = {
"/api/v1/auth/**", "/api/v1/auth/**",
"/api/v1/devis/**",
"/api/v1/grilles-tarifaires/**",
"/actuator/health/**"}; "/actuator/health/**"};
private static final String[] ADMIN_ONLY_URL = {
"/api/v1/users"};
private static final String[] GENERAL_API_URL = {
"/api/v1/quotes/**",
"/api/v1/shipments/**",
"/api/v1/companies/profile"};
private static final String[] INTERNAL_WHITE_LIST_URL = { private static final String[] INTERNAL_WHITE_LIST_URL = {
"/v2/api-docs", "/v2/api-docs",
"/v3/api-docs", "/v3/api-docs",
@ -110,9 +99,10 @@ public class SecurityConfiguration {
http.authorizeHttpRequests(auth -> http.authorizeHttpRequests(auth ->
auth.requestMatchers(WHITE_LIST_URL).permitAll() auth.requestMatchers(WHITE_LIST_URL).permitAll()
.requestMatchers(antMatcher(HttpMethod.GET, "/")).permitAll() .requestMatchers(antMatcher(HttpMethod.GET, "/")).permitAll()
.requestMatchers("/api/v1/users/**").authenticated() .requestMatchers(antMatcher(HttpMethod.GET, API_V1_URI)).permitAll()
.requestMatchers("/api/v1/profile/**").authenticated() .requestMatchers(antMatcher(HttpMethod.PUT, API_V1_URI)).hasRole(ADMIN_ROLE)
.requestMatchers(GENERAL_API_URL).authenticated() .requestMatchers(antMatcher(HttpMethod.DELETE, API_V1_URI)).hasRole(ADMIN_ROLE)
.requestMatchers(antMatcher(HttpMethod.POST, API_V1_URI)).hasRole(ADMIN_ROLE)
.requestMatchers(antMatcher("/h2-console/**")).access(INTERNAL_ACCESS) .requestMatchers(antMatcher("/h2-console/**")).access(INTERNAL_ACCESS)
.requestMatchers(antMatcher("/actuator/**")).access(INTERNAL_ACCESS) .requestMatchers(antMatcher("/actuator/**")).access(INTERNAL_ACCESS)
.requestMatchers(INTERNAL_WHITE_LIST_URL).access(INTERNAL_ACCESS) .requestMatchers(INTERNAL_WHITE_LIST_URL).access(INTERNAL_ACCESS)

View File

@ -1,13 +1,14 @@
---
spring: spring:
h2: h2:
console: console:
enabled: ${SPRING_H2_CONSOLE_ENABLED:false} enabled: 'false'
datasource: datasource:
url: ${SPRING_H2_DATASOURCE_URL:jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE} url: jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driverClassName: ${SPRING_H2_DATASOURCE_DRIVER_CLASS_NAME:org.h2.Driver} driverClassName: org.h2.Driver
username: ${SPRING_H2_DATASOURCE_USERNAME:sa} username: sa
password: ${SPRING_H2_DATASOURCE_PASSWORD:} password: ''
sql: sql:
init: init:
@ -15,74 +16,45 @@ spring:
mode: always mode: always
jpa: jpa:
show-sql: ${SPRING_JPA_SHOW_SQL:false} show-sql: true
properties: properties:
hibernate: hibernate:
format_sql: ${SPRING_JPA_FORMAT_SQL:false} format_sql: true
# show_sql: true # show_sql: true
# database-platform: ${SPRING_JPA_DATABASE_PLATFORM_H2:org.hibernate.dialect.H2Dialect} database-platform: org.hibernate.dialect.H2Dialect
hibernate: hibernate:
ddl-auto: ${SPRING_JPA_HIBERNATE_DDL_AUTO_DEV:create-drop} ddl-auto: update
# Just to load initial data for the demo. DO NOT USE IT IN PRODUCTION # Just to load initial data for the demo. DO NOT USE IT IN PRODUCTION
defer-datasource-initialization: ${SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_DEV:true} defer-datasource-initialization: true
flyway: # flyway automatically uses the datasource from the application to connect to the DB flyway: # flyway automatically uses the datasource from the application to connect to the DB
enabled: ${SPRING_FLYWAY_ENABLED_DEV:false} # enables flyway database migration enabled: false # enables flyway database migration
mail: mail:
host: ${SPRING_MAIL_HOST_DEV:sandbox.smtp.mailtrap.io} host: sandbox.smtp.mailtrap.io
port: ${SPRING_MAIL_PORT_DEV:2525} port: 2525
username: ${SPRING_MAIL_USERNAME_DEV:your_mailtrap_username} username: 2597bd31d265eb
password: ${SPRING_MAIL_PASSWORD_DEV:your_mailtrap_password} password: cd126234193c89
properties: properties:
mail: mail:
smtp: smtp:
ssl: ssl:
trust: ${SPRING_MAIL_SMTP_SSL_TRUST:*} trust:"*"
auth: ${SPRING_MAIL_SMTP_AUTH:true} auth: true
starttls: starttls:
enable: ${SPRING_MAIL_SMTP_STARTTLS_ENABLE:true} enable: true
connectiontimeout: ${SPRING_MAIL_SMTP_CONNECTION_TIMEOUT:5000} connectiontimeout: 5000
timeout: ${SPRING_MAIL_SMTP_TIMEOUT:3000} timeout: 3000
writetimeout: ${SPRING_MAIL_SMTP_WRITE_TIMEOUT:5000} writetimeout: 5000
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID:your-google-client-id}
client-secret: ${GOOGLE_CLIENT_SECRET:your-google-client-secret}
scope: ${GOOGLE_OAUTH2_SCOPE:openid,email,profile}
redirect-uri: ${OAUTH2_REDIRECT_URI_DEV:http://localhost:8080/login/oauth2/code/google}
provider:
google:
authorization-uri: ${GOOGLE_AUTHORIZATION_URI:https://accounts.google.com/o/oauth2/v2/auth}
token-uri: ${GOOGLE_TOKEN_URI:https://oauth2.googleapis.com/token}
user-info-uri: ${GOOGLE_USER_INFO_URI:https://www.googleapis.com/oauth2/v2/userinfo}
user-name-attribute: ${GOOGLE_USER_NAME_ATTRIBUTE:sub}
application: application:
email: email:
from: ${APPLICATION_EMAIL_FROM_DEV:noreply@xpeditis.local} from: randommailjf@gmail.com
csrf: csrf:
enabled: ${APPLICATION_CSRF_ENABLED_DEV:false} enabled: false
security: security:
jwt: jwt:
secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970} secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
expiration: ${JWT_EXPIRATION:86400000} # a day expiration: 86400000 # a day
refresh-token: refresh-token:
expiration: ${JWT_REFRESH_TOKEN_EXPIRATION:604800000} # 7 days expiration: 604800000 # 7 days
oauth2:
google:
enabled: ${APPLICATION_OAUTH2_GOOGLE_ENABLED:true}
license:
trial:
duration-days: ${APPLICATION_LICENSE_TRIAL_DURATION_DAYS:30}
max-users: ${APPLICATION_LICENSE_TRIAL_MAX_USERS:5}
basic:
max-users: ${APPLICATION_LICENSE_BASIC_MAX_USERS:50}
premium:
max-users: ${APPLICATION_LICENSE_PREMIUM_MAX_USERS:200}
enterprise:
max-users: ${APPLICATION_LICENSE_ENTERPRISE_MAX_USERS:1000}

View File

@ -1,9 +1,10 @@
---
spring: spring:
datasource: datasource:
url: ${SPRING_DATASOURCE_URL:} url: ${SPRING_DATASOURCE_URL}
driver-class-name: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver} driver-class-name: com.mysql.cj.jdbc.Driver
username: ${SPRING_DATASOURCE_USERNAME:} username: ${SPRING_DATASOURCE_USERNAME}
password: ${SPRING_DATASOURCE_PASSWORD:} password: ${SPRING_DATASOURCE_PASSWORD}
#hikari: #hikari:
#schema: leblr #schema: leblr
@ -14,83 +15,52 @@ spring:
#data-locations: import_users.sql #data-locations: import_users.sql
jpa: jpa:
show-sql: ${SPRING_JPA_SHOW_SQL:false} # show-sql: true
properties: properties:
hibernate: hibernate:
format_sql: ${SPRING_JPA_FORMAT_SQL:true} format_sql: true
#show_sql: true #show_sql: true
database: mysql database: mysql
database-platform: ${SPRING_JPA_DATABASE_PLATFORM_MYSQL:org.hibernate.dialect.MySQLDialect} database-platform: org.hibernate.dialect.MySQLDialect
hibernate: hibernate:
ddl-auto: ${SPRING_JPA_HIBERNATE_DDL_AUTO_PROD:validate} ddl-auto: validate
defer-datasource-initialization: ${SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_PROD:false} defer-datasource-initialization: false
#open-in-view: false #open-in-view: false
flyway: # flyway automatically uses the datasource from the application to connect to the DB flyway: # flyway automatically uses the datasource from the application to connect to the DB
enabled: ${SPRING_FLYWAY_ENABLED_PROD:true} # enables flyway database migration enabled: true # enables flyway database migration
locations: ${SPRING_FLYWAY_LOCATIONS:classpath:db/migration/structure, classpath:db/migration/data} # the location where flyway should look for migration scripts locations: classpath:db/migration/structure, classpath:db/migration/data # the location where flyway should look for migration scripts
validate-on-migrate: ${SPRING_FLYWAY_VALIDATE_ON_MIGRATE:true} validate-on-migrate: true
baseline-on-migrate: ${SPRING_FLYWAY_BASELINE_ON_MIGRATE:true} baseline-on-migrate: true
baseline-version: ${SPRING_FLYWAY_BASELINE_VERSION:0} baseline-version: 0
default-schema: ${SPRING_FLYWAY_DEFAULT_SCHEMA:leblr} default-schema: leblr
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: ${GOOGLE_OAUTH2_SCOPE:openid,email,profile}
redirect-uri: ${OAUTH2_REDIRECT_URI_PROD:https://xpeditis.fr/login/oauth2/code/google}
provider:
google:
authorization-uri: ${GOOGLE_AUTHORIZATION_URI:https://accounts.google.com/o/oauth2/v2/auth}
token-uri: ${GOOGLE_TOKEN_URI:https://oauth2.googleapis.com/token}
user-info-uri: ${GOOGLE_USER_INFO_URI:https://www.googleapis.com/oauth2/v2/userinfo}
user-name-attribute: ${GOOGLE_USER_NAME_ATTRIBUTE:sub}
mail: mail:
protocol: ${SPRING_MAIL_PROTOCOL_PROD:smtp} protocol: smtp
host: ${SPRING_MAIL_HOST_PROD:ssl0.ovh.net} host: ssl0.ovh.net
port: ${SPRING_MAIL_PORT_PROD:587} port: 587
username: ${SPRING_MAIL_USERNAME_PROD:contact@xpeditis.fr} username: contact@xpeditis.fr
password: ${SPRING_MAIL_PASSWORD_PROD:} password:
properties: properties:
mail: mail:
smtp: smtp:
auth: ${SPRING_MAIL_SMTP_AUTH:true} auth: true
starttls: starttls:
enable: ${SPRING_MAIL_SMTP_STARTTLS_ENABLE:true} enable: true
connectiontimeout: ${SPRING_MAIL_SMTP_CONNECTION_TIMEOUT:5000} connectiontimeout: 5000
timeout: ${SPRING_MAIL_SMTP_TIMEOUT:3000} timeout: 3000
writetimeout: ${SPRING_MAIL_SMTP_WRITE_TIMEOUT:5000} writetimeout: 5000
application: application:
email: email:
from: ${APPLICATION_EMAIL_FROM_PROD:contact@xpeditis.fr} from: contact@leblr.fr
csrf: csrf:
enabled: ${APPLICATION_CSRF_ENABLED_PROD:true} enabled: false
security: security:
jwt: jwt:
secret-key: ${JWT_SECRET_KEY} secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
expiration: ${JWT_EXPIRATION:86400000} # a day expiration: 86400000 # a day
refresh-token: refresh-token:
expiration: ${JWT_REFRESH_TOKEN_EXPIRATION:604800000} # 7 days expiration: 604800000 # 7 days
oauth2:
google:
enabled: ${APPLICATION_OAUTH2_GOOGLE_ENABLED:true}
license:
trial:
duration-days: ${APPLICATION_LICENSE_TRIAL_DURATION_DAYS:30}
max-users: ${APPLICATION_LICENSE_TRIAL_MAX_USERS:5}
basic:
max-users: ${APPLICATION_LICENSE_BASIC_MAX_USERS:50}
premium:
max-users: ${APPLICATION_LICENSE_PREMIUM_MAX_USERS:200}
enterprise:
max-users: ${APPLICATION_LICENSE_ENTERPRISE_MAX_USERS:1000}

View File

@ -1,8 +1,8 @@
server: server:
port: ${SERVER_PORT:8080} port: 8080
file: file:
upload-dir: ${FILE_UPLOAD_DIR:/upload} upload-dir: /upload
spring: spring:
http: http:
@ -12,11 +12,11 @@ spring:
force: true force: true
application: application:
name: ${SPRING_APPLICATION_NAME:@project.description@} name: '@project.description@'
version: ${SPRING_APPLICATION_VERSION:@project.version@} version: '@project.version@'
profiles: profiles:
active: ${SPRING_PROFILES_ACTIVE:@spring.profiles.active@} active: '@spring.profiles.active@'
banner: banner:
location: 'classpath:banner.txt' location: 'classpath:banner.txt'
@ -30,22 +30,21 @@ spring:
servlet: servlet:
multipart: multipart:
enabled: ${SPRING_SERVLET_MULTIPART_ENABLED:true} enabled: true
max-file-size: ${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB} max-file-size: 50MB
max-request-size: ${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB} max-request-size: 50MB
#location: ${java.io.tmpdir} #location: ${java.io.tmpdir}
logging: logging:
level: level:
root: ${LOGGING_LEVEL_ROOT:INFO}
org: org:
org.hibernate.orm.query.sqm.ast.logTree: ${LOGGING_LEVEL_HIBERNATE_SQL:OFF} org.hibernate.orm.query.sqm.ast.logTree: OFF
springframework: springframework:
boot: boot:
autoconfigure: ${LOGGING_LEVEL_SPRINGFRAMEWORK_BOOT_AUTOCONFIGURE:OFF} autoconfigure: OFF
web: web:
filter: filter:
CommonsRequestLoggingFilter: ${LOGGING_LEVEL_COMMONS_REQUEST_LOGGING_FILTER:INFO} CommonsRequestLoggingFilter: INFO
security: security:
config: config:
annotation: annotation:

View File

@ -0,0 +1,14 @@
package com.dh7789dev.xpeditis;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
@ActiveProfiles("dev")
class LeBlrApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -1,24 +1,12 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest; import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse; import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
import com.dh7789dev.xpeditis.port.in.AuthenticationUseCase; import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
public interface AuthenticationService extends AuthenticationUseCase { public interface AuthenticationService {
AuthenticationResponse authenticate(AuthenticationRequest request); AuthenticationResponse authenticate(AuthenticationRequest request);
AuthenticationResponse register(RegisterRequest request); AuthenticationResponse register(RegisterRequest request);
AuthenticationResponse authenticateWithGoogle(String googleToken);
UserAccount getCurrentUser(String token);
void logout(String token);
boolean validateToken(String token);
AuthenticationResponse refreshToken(String refreshToken);
} }

View File

@ -1,24 +1,4 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.dto.app.License;
import com.dh7789dev.xpeditis.dto.request.CreateCompanyRequest;
import com.dh7789dev.xpeditis.dto.request.UpdateCompanyRequest;
import com.dh7789dev.xpeditis.dto.app.LicenseType;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface CompanyService { public interface CompanyService {
Company createCompany(CreateCompanyRequest request);
Company updateCompany(UUID id, UpdateCompanyRequest request);
Optional<Company> findCompanyById(UUID id);
List<Company> findAllCompanies();
void deleteCompany(UUID id);
boolean validateLicense(UUID companyId, int requestedUsers);
boolean isLicenseActive(UUID companyId);
License upgradeLicense(UUID companyId, LicenseType newLicenseType);
int getMaxUsersForLicense(LicenseType licenseType);
} }

View File

@ -1,23 +0,0 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.DemandeDevis;
import com.dh7789dev.xpeditis.dto.app.ReponseDevis;
public interface DevisCalculService {
/**
* Calcule les 3 offres (Rapide, Standard, Économique) pour une demande de devis
*
* @param demandeDevis la demande de devis avec tous les détails
* @return une réponse contenant les 3 offres calculées
*/
ReponseDevis calculerTroisOffres(DemandeDevis demandeDevis);
/**
* Valide qu'une demande de devis contient toutes les informations nécessaires
*
* @param demandeDevis la demande à valider
* @throws IllegalArgumentException si des données obligatoires sont manquantes
*/
void validerDemandeDevis(DemandeDevis demandeDevis);
}

View File

@ -1,88 +0,0 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
import com.dh7789dev.xpeditis.dto.app.DemandeDevis;
import java.util.List;
public interface GrilleTarifaireService {
/**
* Trouve toutes les grilles tarifaires applicables pour une demande de devis
*
* @param demandeDevis la demande de devis
* @return liste des grilles applicables
*/
List<GrilleTarifaire> trouverGrillesApplicables(DemandeDevis demandeDevis);
/**
* Crée ou met à jour une grille tarifaire
*
* @param grilleTarifaire la grille à sauvegarder
* @return la grille sauvegardée
*/
GrilleTarifaire sauvegarderGrille(GrilleTarifaire grilleTarifaire);
/**
* Trouve une grille tarifaire par son ID
*
* @param id l'identifiant de la grille
* @return la grille trouvée ou null
*/
GrilleTarifaire trouverParId(Long id);
/**
* Supprime une grille tarifaire
*
* @param id l'identifiant de la grille à supprimer
*/
void supprimerGrille(Long id);
/**
* Importe des grilles tarifaires depuis un fichier CSV
*
* @param file le fichier CSV
* @param mode le mode d'import (MERGE ou REPLACE)
* @return la liste des grilles importées
*/
List<GrilleTarifaire> importerDepuisCsv(org.springframework.web.multipart.MultipartFile file, String mode) throws java.io.IOException;
/**
* Importe des grilles tarifaires depuis un fichier Excel
*
* @param file le fichier Excel
* @param sheetName le nom de la feuille (optionnel)
* @param mode le mode d'import (MERGE ou REPLACE)
* @return la liste des grilles importées
*/
List<GrilleTarifaire> importerDepuisExcel(org.springframework.web.multipart.MultipartFile file, String sheetName, String mode) throws java.io.IOException;
/**
* Importe des grilles tarifaires depuis du JSON
*
* @param grilles la liste des grilles au format JSON
* @param mode le mode d'import (MERGE ou REPLACE)
* @return la liste des grilles importées
*/
List<GrilleTarifaire> importerDepuisJson(List<GrilleTarifaire> grilles, String mode);
/**
* Liste les grilles tarifaires avec pagination et filtres
*
* @param page numéro de page
* @param size taille de la page
* @param transporteur filtre par transporteur
* @param paysOrigine filtre par pays d'origine
* @param paysDestination filtre par pays de destination
* @return la liste des grilles correspondant aux critères
*/
List<GrilleTarifaire> listerGrilles(int page, int size, String transporteur, String paysOrigine, String paysDestination);
/**
* Valide la structure et le contenu d'un fichier d'import
*
* @param file le fichier à valider
* @return les résultats de validation
*/
java.util.Map<String, Object> validerFichier(org.springframework.web.multipart.MultipartFile file) throws java.io.IOException;
}

View File

@ -1,25 +1,4 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Company; public interface LicenseService {
import com.dh7789dev.xpeditis.dto.app.License;
import com.dh7789dev.xpeditis.dto.app.LicenseType;
import com.dh7789dev.xpeditis.port.in.LicenseValidationUseCase;
import java.util.UUID;
public interface LicenseService extends LicenseValidationUseCase {
boolean validateLicense(UUID companyId);
boolean canAddUser(UUID companyId);
License createTrialLicense(Company company);
License upgradeLicense(UUID companyId, LicenseType newType);
void deactivateLicense(UUID licenseId);
License getActiveLicense(UUID companyId);
long getDaysUntilExpiration(UUID companyId);
} }

View File

@ -1,35 +1,10 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest; import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import com.dh7789dev.xpeditis.port.in.UserManagementUseCase;
import java.security.Principal; import java.security.Principal;
import java.util.Optional;
import java.util.UUID;
public interface UserService extends UserManagementUseCase { public interface UserService {
void changePassword(ChangePasswordRequest request, Principal connectedUser); void changePassword(ChangePasswordRequest request, Principal connectedUser);
UserAccount createUser(RegisterRequest request);
UserAccount createGoogleUser(String googleToken);
Optional<UserAccount> findById(UUID id);
Optional<UserAccount> findByEmail(String email);
Optional<UserAccount> findByUsername(String username);
UserAccount updateProfile(UserAccount userAccount);
void deactivateUser(UUID userId);
void deleteUser(UUID userId);
boolean existsByEmail(String email);
boolean existsByUsername(String username);
} }

View File

@ -1,20 +0,0 @@
package com.dh7789dev.xpeditis.port.in;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
public interface AuthenticationUseCase {
AuthenticationResponse authenticate(AuthenticationRequest request);
AuthenticationResponse authenticateWithGoogle(String googleToken);
UserAccount getCurrentUser(String token);
void logout(String token);
boolean validateToken(String token);
AuthenticationResponse refreshToken(String refreshToken);
}

View File

@ -1,24 +0,0 @@
package com.dh7789dev.xpeditis.port.in;
import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.dto.app.License;
import com.dh7789dev.xpeditis.dto.app.LicenseType;
import java.util.UUID;
public interface LicenseValidationUseCase {
boolean validateLicense(UUID companyId);
boolean canAddUser(UUID companyId);
License createTrialLicense(Company company);
License upgradeLicense(UUID companyId, LicenseType newType);
void deactivateLicense(UUID licenseId);
License getActiveLicense(UUID companyId);
long getDaysUntilExpiration(UUID companyId);
}

View File

@ -1,34 +0,0 @@
package com.dh7789dev.xpeditis.port.in;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import java.security.Principal;
import java.util.Optional;
import java.util.UUID;
public interface UserManagementUseCase {
UserAccount createUser(RegisterRequest request);
UserAccount createGoogleUser(String googleToken);
Optional<UserAccount> findById(UUID id);
Optional<UserAccount> findByEmail(String email);
Optional<UserAccount> findByUsername(String username);
UserAccount updateProfile(UserAccount userAccount);
void changePassword(ChangePasswordRequest request, Principal connectedUser);
void deactivateUser(UUID userId);
void deleteUser(UUID userId);
boolean existsByEmail(String email);
boolean existsByUsername(String username);
}

View File

@ -1,6 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
public enum AuthProvider {
LOCAL,
GOOGLE
}

View File

@ -1,50 +1,19 @@
package com.dh7789dev.xpeditis.dto.app; package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime; import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.List; import java.util.List;
import java.util.UUID;
@Data @Data
@Builder
@NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class Company { public class Company {
private UUID id; private Long id;
private String name; private String name;
private String description;
private String website;
private String industry;
private String country; private String country;
private String siren;
private String numEori;
private String phone;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private boolean isActive;
private List<UserAccount> users; private List<UserAccount> users;
private List<License> licenses;
private License license; // Current active license
private List<Quote> quotes; private List<Quote> quotes;
private List<ExportFolder> exports; private List<ExportFolder> exports;
public License getActiveLicense() {
return licenses != null ? licenses.stream()
.filter(License::isActive)
.filter(license -> license.getExpirationDate() == null ||
license.getExpirationDate().isAfter(LocalDateTime.now().toLocalDate()))
.findFirst()
.orElse(null) : null;
}
public int getActiveUserCount() {
return users != null ? (int) users.stream()
.filter(UserAccount::isActive)
.count() : 0;
}
} }

View File

@ -1,119 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DemandeDevis {
private String typeService;
private String incoterm;
private String typeLivraison;
// Adresses
private AdresseTransport depart;
private AdresseTransport arrivee;
// Douane
private String douaneImportExport;
private Boolean eur1Import;
private Boolean eur1Export;
// Colisage
private List<Colisage> colisages;
// Contraintes et services
private MarchandiseDangereuse marchandiseDangereuse;
private ManutentionParticuliere manutentionParticuliere;
private ProduitsReglementes produitsReglementes;
private ServicesAdditionnels servicesAdditionnels;
// Dates
private LocalDate dateEnlevement;
private LocalDate dateLivraison;
// Client
private String nomClient;
private String emailClient;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class AdresseTransport {
private String ville;
private String codePostal;
private String pays;
private String coordonneesGps;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Colisage {
private TypeColisage type;
private Integer quantite;
private Double longueur;
private Double largeur;
private Double hauteur;
private Double poids;
private Boolean gerbable;
public Double getVolume() {
if (longueur != null && largeur != null && hauteur != null) {
return longueur * largeur * hauteur / 1000000; // cm³ to m³
}
return 0.0;
}
public enum TypeColisage {
CAISSE, COLIS, PALETTE, AUTRES
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class MarchandiseDangereuse {
private String type;
private String classe;
private String unNumber;
private String description;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ManutentionParticuliere {
private Boolean hayon;
private Boolean sangles;
private Boolean couvertureThermique;
private String autres;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ProduitsReglementes {
private Boolean alimentaire;
private Boolean pharmaceutique;
private String autres;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ServicesAdditionnels {
private Boolean rendezVousLivraison;
private Boolean documentT1;
private Boolean stopDouane;
private Boolean assistanceExport;
private Boolean assurance;
private Double valeurDeclaree;
}
}

View File

@ -1,27 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FraisAdditionnels {
private Long id;
private Long grilleId;
private String typeFrais;
private String description;
private BigDecimal montant;
private UniteFacturation uniteFacturation;
private BigDecimal montantMinimum;
private Boolean obligatoire;
private Boolean applicableMarchandiseDangereuse;
public enum UniteFacturation {
LS, KG, M3, PALETTE, POURCENTAGE
}
}

View File

@ -1,21 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoogleUserInfo {
private String id;
private String email;
private String firstName;
private String lastName;
private String picture;
private boolean verified;
private String locale;
}

View File

@ -1,51 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GrilleTarifaire {
private Long id;
private String nomGrille;
private String transporteur;
private TypeService typeService;
private String originePays;
private String origineVille;
private String originePortCode;
private String destinationPays;
private String destinationVille;
private String destinationPortCode;
private String incoterm;
private ModeTransport modeTransport;
private ServiceType serviceType;
private Integer transitTimeMin;
private Integer transitTimeMax;
private LocalDate validiteDebut;
private LocalDate validiteFin;
private String devise;
private Boolean actif;
private String deviseBase;
private String commentaires;
private List<TarifFret> tarifsFret;
private List<FraisAdditionnels> fraisAdditionnels;
private List<SurchargeDangereuse> surchargesDangereuses;
public enum TypeService {
IMPORT, EXPORT
}
public enum ModeTransport {
MARITIME, AERIEN, ROUTIER, FERROVIAIRE
}
public enum ServiceType {
RAPIDE, STANDARD, ECONOMIQUE
}
}

View File

@ -1,54 +1,16 @@
package com.dh7789dev.xpeditis.dto.app; package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;
@Data @Data
@Builder
@NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class License { public class License {
private UUID id; private Long id;
private String licenseKey; private String licenseKey;
private LicenseType type;
private LocalDate startDate;
private LocalDate expirationDate; private LocalDate expirationDate;
private LocalDateTime issuedDate; private boolean active;
private LocalDateTime expiryDate; private UserAccount user;
private int maxUsers;
private boolean isActive;
private LocalDateTime createdAt;
private Company company;
public boolean isExpired() {
return expirationDate != null && expirationDate.isBefore(LocalDate.now());
}
public LocalDateTime getExpiryDate() {
return expiryDate;
}
public boolean isValid() {
return isActive && !isExpired();
}
public boolean canAddUser(int currentUserCount) {
return !hasUserLimit() || currentUserCount < maxUsers;
}
public boolean hasUserLimit() {
return type != null && type.hasUserLimit();
}
public long getDaysUntilExpiration() {
return expirationDate != null ?
java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(), expirationDate) :
Long.MAX_VALUE;
}
} }

View File

@ -1,32 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
public enum LicenseType {
TRIAL(5, 30),
BASIC(10, -1),
PREMIUM(50, -1),
ENTERPRISE(-1, -1);
private final int maxUsers;
private final int durationDays;
LicenseType(int maxUsers, int durationDays) {
this.maxUsers = maxUsers;
this.durationDays = durationDays;
}
public int getMaxUsers() {
return maxUsers;
}
public int getDurationDays() {
return durationDays;
}
public boolean hasUserLimit() {
return maxUsers > 0;
}
public boolean hasTimeLimit() {
return durationDays > 0;
}
}

View File

@ -1,38 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OffreCalculee {
private String type; // RAPIDE, STANDARD, ECONOMIQUE
private BigDecimal prixTotal;
private String devise;
private String transitTime;
private String transporteur;
private String modeTransport;
private List<String> servicesInclus;
private DetailPrix detailPrix;
private LocalDate validite;
private List<String> conditions;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class DetailPrix {
private BigDecimal fretBase;
private Map<String, BigDecimal> fraisFixes;
private Map<String, BigDecimal> servicesOptionnels;
private BigDecimal surchargeDangereuse;
private BigDecimal coefficientService;
}
}

View File

@ -1,65 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReponseDevis {
private String demandeId;
private ClientInfo client;
private DetailsTransport detailsTransport;
private ColisageResume colisageResume;
private List<OffreCalculee> offres;
private Recommandation recommandation;
private List<String> mentionsLegales;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ClientInfo {
private String nom;
private String email;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class DetailsTransport {
private String typeService;
private String incoterm;
private AdresseInfo depart;
private AdresseInfo arrivee;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class AdresseInfo {
private String adresse;
private String coordonnees;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ColisageResume {
private Integer nombreColis;
private Double poidsTotal;
private Double volumeTotal;
private Double poidsTaxable;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Recommandation {
private String offreRecommandee;
private String raison;
}
}

View File

@ -1,8 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
public enum Role {
USER,
MANAGER,
ADMIN,
ADMIN_PLATFORM
}

View File

@ -1,25 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SurchargeDangereuse {
private Long id;
private Long grilleId;
private String classeAdr;
private String unNumber;
private BigDecimal surcharge;
private UniteFacturation uniteFacturation;
private BigDecimal minimum;
public enum UniteFacturation {
LS, KG, COLIS
}
}

View File

@ -1,27 +0,0 @@
package com.dh7789dev.xpeditis.dto.app;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TarifFret {
private Long id;
private Long grilleId;
private BigDecimal poidsMin;
private BigDecimal poidsMax;
private BigDecimal volumeMin;
private BigDecimal volumeMax;
private BigDecimal tauxUnitaire;
private UniteFacturation uniteFacturation;
private BigDecimal minimumFacturation;
public enum UniteFacturation {
KG, M3, PALETTE, COLIS, LS
}
}

View File

@ -1,51 +1,20 @@
package com.dh7789dev.xpeditis.dto.app; package com.dh7789dev.xpeditis.dto.app;
import com.dh7789dev.xpeditis.dto.valueobject.Email;
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.UUID;
@Data @Data
@Builder
@NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class UserAccount { public class UserAccount {
private UUID id; private String username;
private String firstName; private String firstName;
private String lastName; private String lastName;
private Email email; private String email;
private String username;
private String password; private String password;
private PhoneNumber phoneNumber; private String role; // or "ADMIN"
private AuthProvider authProvider;
private String googleId;
private boolean privacyPolicyAccepted;
private LocalDateTime privacyPolicyAcceptedAt;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime lastLoginAt;
private boolean isActive;
private Role role;
private Company company; private Company company;
private License license; private License license;
private List<Quote> quotes; private List<Quote> quotes;
public String getFullName() {
return (firstName != null ? firstName : "") +
(lastName != null ? " " + lastName : "").trim();
}
public boolean isGoogleAuth() {
return AuthProvider.GOOGLE.equals(authProvider);
}
public boolean isLocalAuth() {
return AuthProvider.LOCAL.equals(authProvider);
}
} }

View File

@ -1,28 +0,0 @@
package com.dh7789dev.xpeditis.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class CreateCompanyRequest {
@NotBlank(message = "Company name is required")
@Size(min = 2, max = 100, message = "Company name must be between 2 and 100 characters")
private String name;
@Size(max = 500, message = "Description must not exceed 500 characters")
private String description;
@Size(max = 255, message = "Website must not exceed 255 characters")
private String website;
@Size(max = 100, message = "Industry must not exceed 100 characters")
private String industry;
}

View File

@ -1,22 +0,0 @@
package com.dh7789dev.xpeditis.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class GoogleAuthRequest {
@NotBlank(message = "Google token is required")
String googleToken;
String companyName;
String phoneNumber;
}

View File

@ -1,70 +1,41 @@
package com.dh7789dev.xpeditis.dto.request; package com.dh7789dev.xpeditis.dto.request;
import com.dh7789dev.xpeditis.dto.app.AuthProvider; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.*;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
@Data @Data
@Builder(toBuilder = true)
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Accessors(chain = true) @Accessors(chain = true)
public class RegisterRequest { public class RegisterRequest {
@NotBlank(message = "First name is required") @NotBlank
@Size(max = 50, message = "First name must not exceed 50 characters")
String firstName; String firstName;
@NotBlank(message = "Last name is required") @NotBlank
@Size(max = 50, message = "Last name must not exceed 50 characters")
String lastName; String lastName;
@Email(message = "Invalid email format") @NotBlank
@NotBlank(message = "Email is required")
String email;
@Size(max = 50, message = "Username must not exceed 50 characters")
String username; String username;
@Pattern( @NotBlank
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$", String email;
message = "Password must be at least 8 characters with uppercase, lowercase and digit"
) @NotBlank
String password; String password;
String confirmPassword; @NotBlank
String phone;
@NotBlank(message = "Phone number is required") String company_uuid = "";
@Pattern(
regexp = "^\\+?[1-9]\\d{1,14}$",
message = "Invalid phone number format"
)
String phoneNumber;
@NotBlank(message = "Company name is required") String company_name;
@Size(max = 100, message = "Company name must not exceed 100 characters")
String companyName;
String companyCountry;
@Builder.Default
AuthProvider authProvider = AuthProvider.LOCAL;
String googleId;
@AssertTrue(message = "Privacy policy must be accepted")
boolean privacyPolicyAccepted;
@AssertTrue(message = "Password confirmation must match")
public boolean isPasswordConfirmed() {
return password != null && password.equals(confirmPassword);
}
} }

View File

@ -1,26 +0,0 @@
package com.dh7789dev.xpeditis.dto.request;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UpdateCompanyRequest {
@Size(min = 2, max = 100, message = "Company name must be between 2 and 100 characters")
private String name;
@Size(max = 500, message = "Description must not exceed 500 characters")
private String description;
@Size(max = 255, message = "Website must not exceed 255 characters")
private String website;
@Size(max = 100, message = "Industry must not exceed 100 characters")
private String industry;
}

View File

@ -1,33 +0,0 @@
package com.dh7789dev.xpeditis.dto.request;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UpdateProfileRequest {
@Size(max = 50, message = "First name must not exceed 50 characters")
String firstName;
@Size(max = 50, message = "Last name must not exceed 50 characters")
String lastName;
@Pattern(
regexp = "^\\+?[1-9]\\d{1,14}$",
message = "Invalid phone number format"
)
String phoneNumber;
@Size(max = 50, message = "Username must not exceed 50 characters")
String username;
}

View File

@ -4,7 +4,6 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
@ -13,7 +12,6 @@ import lombok.experimental.FieldDefaults;
import java.util.Date; import java.util.Date;
@Data @Data
@Builder
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@ -27,22 +25,12 @@ public class AuthenticationResponse {
@JsonProperty("refresh_token") @JsonProperty("refresh_token")
String refreshToken; String refreshToken;
@JsonProperty("token_type")
@Builder.Default
String tokenType = "Bearer";
@JsonProperty("expires_in")
long expiresIn;
@JsonProperty("created_at") @JsonProperty("created_at")
Date createdAt; Date createdAt;
@JsonProperty("expires_at") @JsonProperty("expires_at")
Date expiresAt; Date expiresAt;
@JsonProperty("user")
UserResponse user;
@JsonProperty("error_message") @JsonProperty("error_message")
String error; String error;
} }

View File

@ -1,23 +0,0 @@
package com.dh7789dev.xpeditis.dto.response;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class TokenResponse {
String token;
String refreshToken;
long expiresIn;
@Builder.Default
String tokenType = "Bearer";
UserResponse user;
}

View File

@ -1,41 +0,0 @@
package com.dh7789dev.xpeditis.dto.response;
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.time.LocalDateTime;
import java.util.UUID;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UserResponse {
UUID id;
String firstName;
String lastName;
String email;
String username;
String phoneNumber;
AuthProvider authProvider;
boolean privacyPolicyAccepted;
LocalDateTime privacyPolicyAcceptedAt;
LocalDateTime createdAt;
LocalDateTime lastLoginAt;
boolean isActive;
String role;
String companyName;
UUID companyId;
public String getFullName() {
return (firstName != null ? firstName : "") +
(lastName != null ? " " + lastName : "").trim();
}
}

View File

@ -1,29 +0,0 @@
package com.dh7789dev.xpeditis.dto.valueobject;
import lombok.Value;
import java.util.regex.Pattern;
@Value
public class Email {
private static final Pattern EMAIL_PATTERN = Pattern.compile(
"^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$"
);
String value;
public Email(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Email cannot be null or empty");
}
if (!EMAIL_PATTERN.matcher(value.trim()).matches()) {
throw new IllegalArgumentException("Invalid email format: " + value);
}
this.value = value.trim().toLowerCase();
}
@Override
public String toString() {
return value;
}
}

View File

@ -1,33 +0,0 @@
package com.dh7789dev.xpeditis.dto.valueobject;
import lombok.Value;
import java.util.regex.Pattern;
@Value
public class PhoneNumber {
private static final Pattern PHONE_PATTERN = Pattern.compile(
"^\\+?[1-9]\\d{1,14}$"
);
String value;
public PhoneNumber(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Phone number cannot be null or empty");
}
String cleanedPhone = value.replaceAll("[\\s\\-\\(\\)]", "");
if (!PHONE_PATTERN.matcher(cleanedPhone).matches()) {
throw new IllegalArgumentException("Invalid phone number format: " + value);
}
this.value = cleanedPhone;
}
@Override
public String toString() {
return value;
}
}

View File

@ -1,12 +0,0 @@
package com.dh7789dev.xpeditis.exception;
public class AuthenticationException extends RuntimeException {
public AuthenticationException(String message) {
super(message);
}
public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,12 +0,0 @@
package com.dh7789dev.xpeditis.exception;
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,11 +0,0 @@
package com.dh7789dev.xpeditis.exception;
public class CompanyInactiveException extends RuntimeException {
public CompanyInactiveException(String message) {
super(message);
}
public CompanyInactiveException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,11 +0,0 @@
package com.dh7789dev.xpeditis.exception;
public class InvalidCredentialsException extends RuntimeException {
public InvalidCredentialsException(String message) {
super(message);
}
public InvalidCredentialsException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,11 +0,0 @@
package com.dh7789dev.xpeditis.exception;
public class LicenseExpiredException extends RuntimeException {
public LicenseExpiredException(String message) {
super(message);
}
public LicenseExpiredException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,11 +0,0 @@
package com.dh7789dev.xpeditis.exception;
public class LicenseUserLimitExceededException extends RuntimeException {
public LicenseUserLimitExceededException(String message) {
super(message);
}
public LicenseUserLimitExceededException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,12 +0,0 @@
package com.dh7789dev.xpeditis.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,11 +0,0 @@
package com.dh7789dev.xpeditis.exception;
public class UserAlreadyExistsException extends RuntimeException {
public UserAlreadyExistsException(String message) {
super(message);
}
public UserAlreadyExistsException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -62,10 +62,6 @@
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId> <artifactId>spring-security-crypto</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -1,244 +1,26 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.GoogleUserInfo;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest; import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse; import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest; import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import com.dh7789dev.xpeditis.dto.valueobject.Email;
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
import com.dh7789dev.xpeditis.dto.app.Role;
import com.dh7789dev.xpeditis.exception.AuthenticationException;
import com.dh7789dev.xpeditis.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
@Service @Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class AuthenticationServiceImpl implements AuthenticationService { public class AuthenticationServiceImpl implements AuthenticationService {
private final AuthenticationRepository authenticationRepository; private final AuthenticationRepository authenticationRepository;
private final UserRepository userRepository;
private final OAuth2Provider oAuth2Provider; public AuthenticationServiceImpl(AuthenticationRepository authenticationRepository) {
private final CompanyService companyService; this.authenticationRepository = authenticationRepository;
private final PasswordEncoder passwordEncoder; }
@Override @Override
public AuthenticationResponse authenticate(AuthenticationRequest request) { public AuthenticationResponse authenticate(AuthenticationRequest request) {
log.info("Authenticating user with username/email: {}", request.getUsername());
// Validate input
if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
throw new AuthenticationException("Username or email is required");
}
if (request.getPassword() == null || request.getPassword().trim().isEmpty()) {
throw new AuthenticationException("Password is required");
}
return authenticationRepository.authenticate(request); return authenticationRepository.authenticate(request);
} }
@Override @Override
public AuthenticationResponse register(RegisterRequest request) { public AuthenticationResponse register(RegisterRequest request) {
log.info("Registering new user with email: {}", request.getEmail());
// Validate business rules
validateRegistrationRequest(request);
// Check if user already exists
if (userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException("User with this email already exists");
}
if (request.getUsername() != null && userRepository.existsByUsername(request.getUsername())) {
throw new BusinessException("Username already taken");
}
// Create and validate user account
UserAccount userAccount = createUserAccountFromRequest(request);
return authenticationRepository.register(request); return authenticationRepository.register(request);
} }
@Override
public AuthenticationResponse authenticateWithGoogle(String googleToken) {
log.info("Authenticating user with Google OAuth2");
if (googleToken == null || googleToken.trim().isEmpty()) {
throw new AuthenticationException("Google token is required");
}
// Validate Google token
if (!oAuth2Provider.validateToken(googleToken)) {
throw new AuthenticationException("Invalid Google token");
}
// Get user info from Google
Optional<GoogleUserInfo> googleUserInfo = oAuth2Provider.getUserInfo(googleToken);
if (googleUserInfo.isEmpty()) {
throw new AuthenticationException("Failed to retrieve user information from Google");
}
GoogleUserInfo userInfo = googleUserInfo.get();
// Check if user exists by Google ID or email
Optional<UserAccount> existingUser = userRepository.findByGoogleId(userInfo.getId());
if (existingUser.isEmpty()) {
existingUser = userRepository.findByEmail(userInfo.getEmail());
}
UserAccount userAccount;
if (existingUser.isPresent()) {
// Update existing user
userAccount = existingUser.get();
updateUserFromGoogleInfo(userAccount, userInfo);
} else {
// Create new user from Google info
userAccount = createUserAccountFromGoogleInfo(userInfo);
}
// Save/update user
userAccount = userRepository.save(userAccount);
return authenticationRepository.authenticateWithGoogle(userAccount);
}
@Override
public UserAccount getCurrentUser(String token) {
if (token == null || token.trim().isEmpty()) {
throw new AuthenticationException("Token is required");
}
return authenticationRepository.getCurrentUser(token);
}
@Override
public void logout(String token) {
if (token == null || token.trim().isEmpty()) {
throw new AuthenticationException("Token is required");
}
log.info("Logging out user with token");
authenticationRepository.logout(token);
}
@Override
public boolean validateToken(String token) {
if (token == null || token.trim().isEmpty()) {
return false;
}
return authenticationRepository.validateToken(token);
}
@Override
public AuthenticationResponse refreshToken(String refreshToken) {
if (refreshToken == null || refreshToken.trim().isEmpty()) {
throw new AuthenticationException("Refresh token is required");
}
log.info("Refreshing token");
return authenticationRepository.refreshToken(refreshToken);
}
private void validateRegistrationRequest(RegisterRequest request) {
if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
throw new BusinessException("Email is required");
}
if (request.getFirstName() == null || request.getFirstName().trim().isEmpty()) {
throw new BusinessException("First name is required");
}
if (request.getLastName() == null || request.getLastName().trim().isEmpty()) {
throw new BusinessException("Last name is required");
}
if (request.getPassword() == null || request.getPassword().length() < 8) {
throw new BusinessException("Password must be at least 8 characters long");
}
// Validate email format by creating Email value object
try {
new Email(request.getEmail());
} catch (IllegalArgumentException e) {
throw new BusinessException("Invalid email format");
}
// Validate phone number if provided
if (request.getPhoneNumber() != null && !request.getPhoneNumber().trim().isEmpty()) {
try {
new PhoneNumber(request.getPhoneNumber());
} catch (IllegalArgumentException e) {
throw new BusinessException("Invalid phone number format");
}
}
}
private UserAccount createUserAccountFromRequest(RegisterRequest request) {
return UserAccount.builder()
.id(UUID.randomUUID())
.firstName(request.getFirstName().trim())
.lastName(request.getLastName().trim())
.email(new Email(request.getEmail()))
.phoneNumber(request.getPhoneNumber() != null ? new PhoneNumber(request.getPhoneNumber()) : null)
.username(request.getUsername() != null ? request.getUsername().trim() : null)
.password(passwordEncoder.encode(request.getPassword()))
.authProvider(AuthProvider.LOCAL)
.role(Role.USER)
.isActive(true)
.privacyPolicyAccepted(request.isPrivacyPolicyAccepted())
.privacyPolicyAcceptedAt(request.isPrivacyPolicyAccepted() ? LocalDateTime.now() : null)
.createdAt(LocalDateTime.now())
.build();
}
private UserAccount createUserAccountFromGoogleInfo(GoogleUserInfo googleInfo) {
return UserAccount.builder()
.id(UUID.randomUUID())
.firstName(googleInfo.getFirstName())
.lastName(googleInfo.getLastName())
.email(new Email(googleInfo.getEmail()))
.googleId(googleInfo.getId())
.authProvider(AuthProvider.GOOGLE)
.role(Role.USER)
.isActive(true)
.privacyPolicyAccepted(false) // User will need to accept on first login
.createdAt(LocalDateTime.now())
.lastLoginAt(LocalDateTime.now())
.build();
}
private void updateUserFromGoogleInfo(UserAccount userAccount, GoogleUserInfo googleInfo) {
// Update Google ID if not set
if (userAccount.getGoogleId() == null) {
userAccount.setGoogleId(googleInfo.getId());
}
// Update auth provider if it was local
if (userAccount.getAuthProvider() == AuthProvider.LOCAL) {
userAccount.setAuthProvider(AuthProvider.GOOGLE);
}
// Update last login time
userAccount.setLastLoginAt(LocalDateTime.now());
// Optionally update profile information if changed
if (!googleInfo.getFirstName().equals(userAccount.getFirstName())) {
userAccount.setFirstName(googleInfo.getFirstName());
}
if (!googleInfo.getLastName().equals(userAccount.getLastName())) {
userAccount.setLastName(googleInfo.getLastName());
}
}
} }

View File

@ -1,314 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.dto.app.License;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.CreateCompanyRequest;
import com.dh7789dev.xpeditis.dto.request.UpdateCompanyRequest;
import com.dh7789dev.xpeditis.dto.app.LicenseType;
import com.dh7789dev.xpeditis.exception.BusinessException;
import com.dh7789dev.xpeditis.exception.ResourceNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Service @Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class CompanyServiceImpl implements CompanyService { public class CompanyServiceImpl implements CompanyService {
private final CompanyRepository companyRepository;
private final LicenseRepository licenseRepository;
private final UserRepository userRepository;
@Value("${application.license.trial.duration-days:30}")
private int trialDurationDays;
@Value("${application.license.trial.max-users:5}")
private int trialMaxUsers;
@Value("${application.license.basic.max-users:50}")
private int basicMaxUsers;
@Value("${application.license.premium.max-users:200}")
private int premiumMaxUsers;
@Value("${application.license.enterprise.max-users:1000}")
private int enterpriseMaxUsers;
@Override
public Company createCompany(CreateCompanyRequest request) {
log.info("Creating company: {}", request.getName());
// Validate business rules
validateCreateCompanyRequest(request);
// Check if company name is unique
if (companyRepository.existsByName(request.getName())) {
throw new BusinessException("Company with this name already exists");
}
// Create company
Company company = Company.builder()
.id(UUID.randomUUID())
.name(request.getName().trim())
.description(request.getDescription() != null ? request.getDescription().trim() : null)
.website(request.getWebsite() != null ? request.getWebsite().trim() : null)
.industry(request.getIndustry() != null ? request.getIndustry().trim() : null)
.isActive(true)
.createdAt(LocalDateTime.now())
.build();
// Save company
company = companyRepository.save(company);
// Create trial license automatically
License trialLicense = createTrialLicense(company);
company.setLicense(trialLicense);
return company;
}
@Override
public Company updateCompany(UUID id, UpdateCompanyRequest request) {
log.info("Updating company with ID: {}", id);
Company company = getCompanyById(id);
// Validate business rules
validateUpdateCompanyRequest(request);
// Check if new name is unique (excluding current company)
if (request.getName() != null && !request.getName().equals(company.getName())) {
if (companyRepository.existsByName(request.getName())) {
throw new BusinessException("Company with this name already exists");
}
company.setName(request.getName().trim());
}
// Update fields if provided
if (request.getDescription() != null) {
company.setDescription(request.getDescription().trim());
}
if (request.getWebsite() != null) {
company.setWebsite(request.getWebsite().trim());
}
if (request.getIndustry() != null) {
company.setIndustry(request.getIndustry().trim());
}
return companyRepository.save(company);
}
@Override
public Optional<Company> findCompanyById(UUID id) {
return companyRepository.findById(id);
}
@Override
public List<Company> findAllCompanies() {
return companyRepository.findAll();
}
@Override
public void deleteCompany(UUID id) {
log.info("Deleting company with ID: {}", id);
Company company = getCompanyById(id);
// Check if company has active users
List<UserAccount> activeUsers = userRepository.findByCompanyIdAndIsActive(id, true);
if (!activeUsers.isEmpty()) {
throw new BusinessException("Cannot delete company with active users. Please deactivate or transfer users first.");
}
companyRepository.deleteById(id);
}
@Override
public boolean validateLicense(UUID companyId, int requestedUsers) {
Optional<Company> companyOpt = companyRepository.findById(companyId);
if (companyOpt.isEmpty()) {
return false;
}
Company company = companyOpt.get();
License license = company.getLicense();
if (license == null) {
log.warn("Company {} has no license", companyId);
return false;
}
return validateLicenseConstraints(license, requestedUsers);
}
@Override
public boolean isLicenseActive(UUID companyId) {
Optional<Company> companyOpt = companyRepository.findById(companyId);
if (companyOpt.isEmpty()) {
return false;
}
Company company = companyOpt.get();
License license = company.getLicense();
if (license == null) {
return false;
}
return license.isActive() && license.getExpiryDate().isAfter(LocalDateTime.now());
}
@Override
public License upgradeLicense(UUID companyId, LicenseType newLicenseType) {
log.info("Upgrading license for company {} to {}", companyId, newLicenseType);
Company company = getCompanyById(companyId);
License currentLicense = company.getLicense();
if (currentLicense == null) {
throw new BusinessException("Company has no current license");
}
// Validate upgrade is allowed
if (!isUpgradeAllowed(currentLicense.getType(), newLicenseType)) {
throw new BusinessException("License upgrade from " + currentLicense.getType() + " to " + newLicenseType + " is not allowed");
}
// Create new license
License newLicense = createLicense(company, newLicenseType);
// Deactivate old license
currentLicense.setActive(false);
licenseRepository.save(currentLicense);
company.setLicense(newLicense);
companyRepository.save(company);
return newLicense;
}
@Override
public int getMaxUsersForLicense(LicenseType licenseType) {
return switch (licenseType) {
case TRIAL -> trialMaxUsers;
case BASIC -> basicMaxUsers;
case PREMIUM -> premiumMaxUsers;
case ENTERPRISE -> enterpriseMaxUsers;
};
}
private Company getCompanyById(UUID id) {
return companyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Company not found with ID: " + id));
}
private void validateCreateCompanyRequest(CreateCompanyRequest request) {
if (request.getName() == null || request.getName().trim().isEmpty()) {
throw new BusinessException("Company name is required");
}
if (request.getName().trim().length() < 2) {
throw new BusinessException("Company name must be at least 2 characters long");
}
if (request.getName().trim().length() > 100) {
throw new BusinessException("Company name must not exceed 100 characters");
}
}
private void validateUpdateCompanyRequest(UpdateCompanyRequest request) {
if (request.getName() != null && request.getName().trim().isEmpty()) {
throw new BusinessException("Company name cannot be empty");
}
if (request.getName() != null && request.getName().trim().length() < 2) {
throw new BusinessException("Company name must be at least 2 characters long");
}
if (request.getName() != null && request.getName().trim().length() > 100) {
throw new BusinessException("Company name must not exceed 100 characters");
}
}
private License createTrialLicense(Company company) {
License license = License.builder()
.id(UUID.randomUUID())
.company(company)
.type(LicenseType.TRIAL)
.issuedDate(LocalDateTime.now())
.expiryDate(LocalDateTime.now().plusDays(trialDurationDays))
.maxUsers(trialMaxUsers)
.isActive(true)
.build();
return licenseRepository.save(license);
}
private License createLicense(Company company, LicenseType licenseType) {
LocalDateTime expiryDate = calculateExpiryDate(licenseType);
int maxUsers = getMaxUsersForLicense(licenseType);
License license = License.builder()
.id(UUID.randomUUID())
.company(company)
.type(licenseType)
.issuedDate(LocalDateTime.now())
.expiryDate(expiryDate)
.maxUsers(maxUsers)
.isActive(true)
.build();
return licenseRepository.save(license);
}
private LocalDateTime calculateExpiryDate(LicenseType licenseType) {
return switch (licenseType) {
case TRIAL -> LocalDateTime.now().plusDays(trialDurationDays);
case BASIC, PREMIUM -> LocalDateTime.now().plusYears(1);
case ENTERPRISE -> LocalDateTime.now().plusYears(2);
};
}
private boolean validateLicenseConstraints(License license, int requestedUsers) {
// Check if license is active
if (!license.isActive()) {
log.warn("License is not active for company {}", license.getCompany().getId());
return false;
}
// Check if license has expired
if (license.getExpiryDate().isBefore(LocalDateTime.now())) {
log.warn("License has expired for company {}", license.getCompany().getId());
return false;
}
// Check user limit
if (requestedUsers > license.getMaxUsers()) {
log.warn("Requested users {} exceeds license limit {} for company {}",
requestedUsers, license.getMaxUsers(), license.getCompany().getId());
return false;
}
return true;
}
private boolean isUpgradeAllowed(LicenseType current, LicenseType target) {
// Define upgrade paths
return switch (current) {
case TRIAL -> target == LicenseType.BASIC || target == LicenseType.PREMIUM || target == LicenseType.ENTERPRISE;
case BASIC -> target == LicenseType.PREMIUM || target == LicenseType.ENTERPRISE;
case PREMIUM -> target == LicenseType.ENTERPRISE;
case ENTERPRISE -> false; // Cannot upgrade from enterprise
};
}
} }

View File

@ -1,478 +0,0 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class DevisCalculServiceImpl implements DevisCalculService {
private final GrilleTarifaireService grilleTarifaireService;
private static final BigDecimal COEFFICIENT_POIDS_VOLUMETRIQUE = new BigDecimal("250"); // 250kg/m³
@Override
public ReponseDevis calculerTroisOffres(DemandeDevis demandeDevis) {
log.info("Calcul des 3 offres pour demande devis client: {}", demandeDevis.getNomClient());
validerDemandeDevis(demandeDevis);
// Calculer le colisage résumé
ReponseDevis.ColisageResume colisageResume = calculerColisageResume(demandeDevis);
// Trouver les grilles applicables
List<GrilleTarifaire> grillesApplicables = grilleTarifaireService.trouverGrillesApplicables(demandeDevis);
if (grillesApplicables.isEmpty()) {
throw new IllegalStateException("Aucune grille tarifaire applicable pour cette demande");
}
// Générer les 3 offres
List<OffreCalculee> offres = genererTroisOffres(demandeDevis, grillesApplicables, colisageResume);
// Déterminer la recommandation
ReponseDevis.Recommandation recommandation = determinerRecommandation(offres);
return new ReponseDevis(
generateDemandeId(),
new ReponseDevis.ClientInfo(demandeDevis.getNomClient(), demandeDevis.getEmailClient()),
mapperDetailsTransport(demandeDevis),
colisageResume,
offres,
recommandation,
getMentionsLegales()
);
}
private List<OffreCalculee> genererTroisOffres(
DemandeDevis demande,
List<GrilleTarifaire> grilles,
ReponseDevis.ColisageResume colisage) {
List<OffreCalculee> offres = new ArrayList<>();
// Pour chaque type de service (Rapide, Standard, Économique)
for (GrilleTarifaire.ServiceType serviceType : GrilleTarifaire.ServiceType.values()) {
// Filtrer les grilles par type de service
List<GrilleTarifaire> grillesService = grilles.stream()
.filter(g -> g.getServiceType() == serviceType)
.collect(Collectors.toList());
// Si pas de grilles spécifiques, utiliser les grilles standard
if (grillesService.isEmpty()) {
grillesService = grilles.stream()
.filter(g -> g.getServiceType() == GrilleTarifaire.ServiceType.STANDARD)
.collect(Collectors.toList());
}
// Calculer la meilleure offre pour ce type de service
OffreCalculee meilleureOffre = calculerMeilleureOffre(demande, grillesService, serviceType, colisage);
if (meilleureOffre != null) {
offres.add(appliquerAjustementParType(meilleureOffre, serviceType));
}
}
return offres;
}
private OffreCalculee calculerMeilleureOffre(
DemandeDevis demande,
List<GrilleTarifaire> grilles,
GrilleTarifaire.ServiceType serviceType,
ReponseDevis.ColisageResume colisage) {
OffreCalculee meilleureOffre = null;
BigDecimal prixMinimal = BigDecimal.valueOf(Double.MAX_VALUE);
for (GrilleTarifaire grille : grilles) {
try {
OffreCalculee offre = calculerOffreGrille(demande, grille, serviceType, colisage);
if (offre != null && offre.getPrixTotal().compareTo(prixMinimal) < 0) {
prixMinimal = offre.getPrixTotal();
meilleureOffre = offre;
}
} catch (Exception e) {
log.warn("Erreur lors du calcul avec la grille {}: {}", grille.getId(), e.getMessage());
}
}
return meilleureOffre;
}
private OffreCalculee calculerOffreGrille(
DemandeDevis demande,
GrilleTarifaire grille,
GrilleTarifaire.ServiceType serviceType,
ReponseDevis.ColisageResume colisage) {
// 1. Calculer le fret de base
BigDecimal fretBase = calculerFretBase(grille, colisage);
// 2. Calculer les frais fixes obligatoires
Map<String, BigDecimal> fraisFixes = calculerFraisFixes(grille, demande, colisage);
// 3. Calculer les surcharges marchandises dangereuses
BigDecimal surchargeDangereuse = calculerSurchargeDangereuse(grille, demande, colisage);
// 4. Calculer les services optionnels demandés
Map<String, BigDecimal> servicesOptionnels = calculerServicesOptionnels(grille, demande, colisage);
// 5. Calculer le prix total
BigDecimal prixTotal = fretBase
.add(fraisFixes.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add))
.add(surchargeDangereuse)
.add(servicesOptionnels.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add));
// Créer les détails du prix
OffreCalculee.DetailPrix detailPrix = new OffreCalculee.DetailPrix();
detailPrix.setFretBase(fretBase);
detailPrix.setFraisFixes(fraisFixes);
detailPrix.setServicesOptionnels(servicesOptionnels);
detailPrix.setSurchargeDangereuse(surchargeDangereuse);
detailPrix.setCoefficientService(BigDecimal.ONE);
// Créer l'offre
OffreCalculee offre = new OffreCalculee();
offre.setType(serviceType.name());
offre.setPrixTotal(prixTotal);
offre.setDevise(grille.getDevise());
offre.setTransitTime(formatTransitTime(grille));
offre.setTransporteur(grille.getTransporteur());
offre.setModeTransport(grille.getModeTransport().name());
offre.setServicesInclus(getServicesInclus(serviceType));
offre.setDetailPrix(detailPrix);
offre.setValidite(LocalDate.now().plusDays(30));
offre.setConditions(getConditions(serviceType));
return offre;
}
private BigDecimal calculerFretBase(GrilleTarifaire grille, ReponseDevis.ColisageResume colisage) {
// Trouver le tarif applicable selon le poids taxable
TarifFret tarifApplicable = grille.getTarifsFret().stream()
.filter(t -> {
BigDecimal poidsTaxable = BigDecimal.valueOf(colisage.getPoidsTaxable());
return (t.getPoidsMin() == null || t.getPoidsMin().compareTo(poidsTaxable) <= 0) &&
(t.getPoidsMax() == null || t.getPoidsMax().compareTo(poidsTaxable) >= 0);
})
.findFirst()
.orElse(null);
if (tarifApplicable == null) {
throw new IllegalStateException("Aucun tarif applicable pour le poids taxable: " + colisage.getPoidsTaxable());
}
// Calculer le coût selon l'unité de facturation
BigDecimal cout = BigDecimal.ZERO;
switch (tarifApplicable.getUniteFacturation()) {
case KG:
cout = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(tarifApplicable.getTauxUnitaire());
break;
case M3:
cout = BigDecimal.valueOf(colisage.getVolumeTotal()).multiply(tarifApplicable.getTauxUnitaire());
break;
case COLIS:
cout = BigDecimal.valueOf(colisage.getNombreColis()).multiply(tarifApplicable.getTauxUnitaire());
break;
case LS:
cout = tarifApplicable.getTauxUnitaire();
break;
}
// Appliquer le minimum de facturation si défini
if (tarifApplicable.getMinimumFacturation() != null &&
cout.compareTo(tarifApplicable.getMinimumFacturation()) < 0) {
cout = tarifApplicable.getMinimumFacturation();
}
return cout.setScale(2, RoundingMode.HALF_UP);
}
private Map<String, BigDecimal> calculerFraisFixes(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
Map<String, BigDecimal> fraisFixes = new HashMap<>();
for (FraisAdditionnels frais : grille.getFraisAdditionnels()) {
if (frais.getObligatoire()) {
BigDecimal montant = calculerMontantFrais(frais, demande, colisage);
fraisFixes.put(frais.getTypeFrais(), montant);
}
}
return fraisFixes;
}
private BigDecimal calculerSurchargeDangereuse(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
if (demande.getMarchandiseDangereuse() == null) {
return BigDecimal.ZERO;
}
SurchargeDangereuse surcharge = grille.getSurchargesDangereuses().stream()
.filter(s -> s.getClasseAdr().equals(demande.getMarchandiseDangereuse().getClasse()))
.findFirst()
.orElse(null);
if (surcharge == null) {
return BigDecimal.ZERO;
}
BigDecimal montant = BigDecimal.ZERO;
switch (surcharge.getUniteFacturation()) {
case KG:
montant = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(surcharge.getSurcharge());
break;
case COLIS:
montant = BigDecimal.valueOf(colisage.getNombreColis()).multiply(surcharge.getSurcharge());
break;
case LS:
montant = surcharge.getSurcharge();
break;
}
if (surcharge.getMinimum() != null && montant.compareTo(surcharge.getMinimum()) < 0) {
montant = surcharge.getMinimum();
}
return montant.setScale(2, RoundingMode.HALF_UP);
}
private Map<String, BigDecimal> calculerServicesOptionnels(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
Map<String, BigDecimal> services = new HashMap<>();
// Assurance
if (demande.getServicesAdditionnels() != null && demande.getServicesAdditionnels().getAssurance()) {
FraisAdditionnels fraisAssurance = grille.getFraisAdditionnels().stream()
.filter(f -> "ASSURANCE".equals(f.getTypeFrais()))
.findFirst()
.orElse(null);
if (fraisAssurance != null) {
BigDecimal montant = calculerMontantFrais(fraisAssurance, demande, colisage);
services.put("assurance", montant);
}
}
// Hayon
if (demande.getManutentionParticuliere() != null && demande.getManutentionParticuliere().getHayon()) {
FraisAdditionnels fraisHayon = grille.getFraisAdditionnels().stream()
.filter(f -> "HAYON".equals(f.getTypeFrais()))
.findFirst()
.orElse(null);
if (fraisHayon != null) {
BigDecimal montant = calculerMontantFrais(fraisHayon, demande, colisage);
services.put("hayon", montant);
}
}
return services;
}
private BigDecimal calculerMontantFrais(FraisAdditionnels frais, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
BigDecimal montant = BigDecimal.ZERO;
switch (frais.getUniteFacturation()) {
case LS:
montant = frais.getMontant();
break;
case KG:
montant = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(frais.getMontant());
break;
case M3:
montant = BigDecimal.valueOf(colisage.getVolumeTotal()).multiply(frais.getMontant());
break;
case POURCENTAGE:
// Pourcentage du fret de base - à implémenter selon le contexte
montant = frais.getMontant();
break;
}
if (frais.getMontantMinimum() != null && montant.compareTo(frais.getMontantMinimum()) < 0) {
montant = frais.getMontantMinimum();
}
return montant.setScale(2, RoundingMode.HALF_UP);
}
private OffreCalculee appliquerAjustementParType(OffreCalculee offre, GrilleTarifaire.ServiceType serviceType) {
BigDecimal coefficient = getCoefficient(serviceType);
Double reductionTransit = getReductionTransitTime(serviceType);
// Appliquer le coefficient au prix total
BigDecimal prixAjuste = offre.getPrixTotal().multiply(coefficient).setScale(2, RoundingMode.HALF_UP);
// Ajuster le détail des prix
offre.getDetailPrix().setCoefficientService(coefficient);
// Mettre à jour le prix total
offre.setPrixTotal(prixAjuste);
// Ajuster le transit time si défini
if (reductionTransit != null) {
String transitTimeAjuste = ajusterTransitTime(offre.getTransitTime(), reductionTransit);
offre.setTransitTime(transitTimeAjuste);
}
return offre;
}
private BigDecimal getCoefficient(GrilleTarifaire.ServiceType serviceType) {
switch (serviceType) {
case RAPIDE:
return new BigDecimal("1.15"); // +15%
case STANDARD:
return BigDecimal.ONE;
case ECONOMIQUE:
return new BigDecimal("0.85"); // -15%
default:
return BigDecimal.ONE;
}
}
private Double getReductionTransitTime(GrilleTarifaire.ServiceType serviceType) {
switch (serviceType) {
case RAPIDE:
return 0.7; // -30%
case STANDARD:
return null; // Pas de changement
case ECONOMIQUE:
return 1.3; // +30%
default:
return null;
}
}
private ReponseDevis.ColisageResume calculerColisageResume(DemandeDevis demande) {
double poidsTotal = demande.getColisages().stream()
.mapToDouble(c -> c.getPoids() * c.getQuantite())
.sum();
double volumeTotal = demande.getColisages().stream()
.mapToDouble(c -> c.getVolume() * c.getQuantite())
.sum();
int nombreColis = demande.getColisages().stream()
.mapToInt(DemandeDevis.Colisage::getQuantite)
.sum();
// Calculer le poids taxable (max entre poids réel et poids volumétrique)
double poidsVolumetrique = volumeTotal * COEFFICIENT_POIDS_VOLUMETRIQUE.doubleValue();
double poidsTaxable = Math.max(poidsTotal, poidsVolumetrique);
return new ReponseDevis.ColisageResume(nombreColis, poidsTotal, volumeTotal, poidsTaxable);
}
private List<String> getServicesInclus(GrilleTarifaire.ServiceType serviceType) {
switch (serviceType) {
case RAPIDE:
return Arrays.asList("Suivi en temps réel", "Assurance de base", "Service express");
case STANDARD:
return Arrays.asList("Suivi standard");
case ECONOMIQUE:
return Collections.emptyList();
default:
return Collections.emptyList();
}
}
private List<String> getConditions(GrilleTarifaire.ServiceType serviceType) {
switch (serviceType) {
case RAPIDE:
return Arrays.asList("Prix valable sous réserve d'espace disponible", "Marchandise prête à l'enlèvement");
case STANDARD:
return Arrays.asList("Prix standard selon grille tarifaire", "Délais indicatifs");
case ECONOMIQUE:
return Arrays.asList("Tarif économique avec délais étendus", "Services minimaux inclus");
default:
return Collections.emptyList();
}
}
private ReponseDevis.Recommandation determinerRecommandation(List<OffreCalculee> offres) {
// Par défaut, recommander l'offre STANDARD
return new ReponseDevis.Recommandation("STANDARD", "Meilleur rapport qualité/prix/délai");
}
private ReponseDevis.DetailsTransport mapperDetailsTransport(DemandeDevis demande) {
ReponseDevis.DetailsTransport.AdresseInfo depart = new ReponseDevis.DetailsTransport.AdresseInfo(
demande.getDepart().getVille() + ", " + demande.getDepart().getCodePostal() + ", " + demande.getDepart().getPays(),
demande.getDepart().getCoordonneesGps()
);
ReponseDevis.DetailsTransport.AdresseInfo arrivee = new ReponseDevis.DetailsTransport.AdresseInfo(
demande.getArrivee().getVille() + ", " + demande.getArrivee().getCodePostal() + ", " + demande.getArrivee().getPays(),
demande.getArrivee().getCoordonneesGps()
);
return new ReponseDevis.DetailsTransport(demande.getTypeService(), demande.getIncoterm(), depart, arrivee);
}
private String formatTransitTime(GrilleTarifaire grille) {
if (grille.getTransitTimeMin() != null && grille.getTransitTimeMax() != null) {
return grille.getTransitTimeMin() + "-" + grille.getTransitTimeMax() + " jours";
}
return "À définir";
}
private String ajusterTransitTime(String transitTime, double coefficient) {
// Extraction des nombres du format "25-30 jours" et application du coefficient
if (transitTime.contains("-")) {
String[] parts = transitTime.split("-");
try {
int min = (int) (Integer.parseInt(parts[0]) * coefficient);
int max = (int) (Integer.parseInt(parts[1].split(" ")[0]) * coefficient);
return min + "-" + max + " jours";
} catch (NumberFormatException e) {
return transitTime;
}
}
return transitTime;
}
private String generateDemandeId() {
return "DEV-" + LocalDate.now().getYear() + "-" + String.format("%06d", new Random().nextInt(999999));
}
private List<String> getMentionsLegales() {
return Arrays.asList(
"Prix hors taxes applicables",
"Conditions générales de vente applicables",
"Devis valable 30 jours"
);
}
@Override
public void validerDemandeDevis(DemandeDevis demandeDevis) {
if (demandeDevis == null) {
throw new IllegalArgumentException("La demande de devis ne peut pas être nulle");
}
if (demandeDevis.getDepart() == null || demandeDevis.getArrivee() == null) {
throw new IllegalArgumentException("Les adresses de départ et d'arrivée sont obligatoires");
}
if (demandeDevis.getColisages() == null || demandeDevis.getColisages().isEmpty()) {
throw new IllegalArgumentException("Au moins un colisage doit être défini");
}
for (DemandeDevis.Colisage colisage : demandeDevis.getColisages()) {
if (colisage.getPoids() == null || colisage.getPoids() <= 0) {
throw new IllegalArgumentException("Le poids de chaque colisage doit être supérieur à 0");
}
}
}
}

View File

@ -1,426 +0,0 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.DemandeDevis;
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class GrilleTarifaireServiceImpl implements GrilleTarifaireService {
private final GrilleTarifaireRepository grilleTarifaireRepository;
@Override
public List<GrilleTarifaire> trouverGrillesApplicables(DemandeDevis demandeDevis) {
log.info("Recherche des grilles tarifaires applicables pour {} -> {}",
demandeDevis.getDepart().getPays(),
demandeDevis.getArrivee().getPays());
LocalDate dateValidite = demandeDevis.getDateEnlevement() != null
? demandeDevis.getDateEnlevement()
: LocalDate.now();
List<GrilleTarifaire> grilles = grilleTarifaireRepository.findGrillesApplicables(
demandeDevis.getTypeService(),
demandeDevis.getDepart().getPays(),
demandeDevis.getArrivee().getPays(),
dateValidite
);
// Filtrer par ville si spécifiée
if (demandeDevis.getDepart().getVille() != null || demandeDevis.getArrivee().getVille() != null) {
grilles = grilles.stream()
.filter(g -> isVilleCompatible(g, demandeDevis))
.collect(Collectors.toList());
}
// Filtrer par incoterm si spécifié
if (demandeDevis.getIncoterm() != null) {
grilles = grilles.stream()
.filter(g -> g.getIncoterm() == null || g.getIncoterm().equals(demandeDevis.getIncoterm()))
.collect(Collectors.toList());
}
log.info("Trouvé {} grille(s) applicables", grilles.size());
return grilles;
}
@Override
public GrilleTarifaire sauvegarderGrille(GrilleTarifaire grilleTarifaire) {
log.info("Sauvegarde de la grille tarifaire: {}", grilleTarifaire.getNomGrille());
validerGrilleTarifaire(grilleTarifaire);
return grilleTarifaireRepository.save(grilleTarifaire);
}
@Override
public GrilleTarifaire trouverParId(Long id) {
log.debug("Recherche de la grille tarifaire avec l'ID: {}", id);
return grilleTarifaireRepository.findById(id)
.orElse(null);
}
@Override
public void supprimerGrille(Long id) {
log.info("Suppression de la grille tarifaire avec l'ID: {}", id);
if (!grilleTarifaireRepository.existsById(id)) {
throw new IllegalArgumentException("Aucune grille tarifaire trouvée avec l'ID: " + id);
}
grilleTarifaireRepository.deleteById(id);
}
private boolean isVilleCompatible(GrilleTarifaire grille, DemandeDevis demande) {
// Si la grille n'a pas de ville spécifiée, elle est compatible avec toutes les villes
boolean origineCompatible = grille.getOrigineVille() == null ||
grille.getOrigineVille().equalsIgnoreCase(demande.getDepart().getVille());
boolean destinationCompatible = grille.getDestinationVille() == null ||
grille.getDestinationVille().equalsIgnoreCase(demande.getArrivee().getVille());
return origineCompatible && destinationCompatible;
}
private void validerGrilleTarifaire(GrilleTarifaire grille) {
if (grille == null) {
throw new IllegalArgumentException("La grille tarifaire ne peut pas être nulle");
}
if (grille.getNomGrille() == null || grille.getNomGrille().trim().isEmpty()) {
throw new IllegalArgumentException("Le nom de la grille est obligatoire");
}
if (grille.getTransporteur() == null || grille.getTransporteur().trim().isEmpty()) {
throw new IllegalArgumentException("Le transporteur est obligatoire");
}
if (grille.getTypeService() == null) {
throw new IllegalArgumentException("Le type de service est obligatoire");
}
if (grille.getOriginePays() == null || grille.getOriginePays().trim().isEmpty()) {
throw new IllegalArgumentException("Le pays d'origine est obligatoire");
}
if (grille.getDestinationPays() == null || grille.getDestinationPays().trim().isEmpty()) {
throw new IllegalArgumentException("Le pays de destination est obligatoire");
}
if (grille.getValiditeDebut() == null) {
throw new IllegalArgumentException("La date de début de validité est obligatoire");
}
if (grille.getValiditeFin() == null) {
throw new IllegalArgumentException("La date de fin de validité est obligatoire");
}
if (grille.getValiditeDebut().isAfter(grille.getValiditeFin())) {
throw new IllegalArgumentException("La date de début doit être antérieure à la date de fin");
}
// Validation des tarifs de fret
if (grille.getTarifsFret() == null || grille.getTarifsFret().isEmpty()) {
throw new IllegalArgumentException("Au moins un tarif de fret doit être défini");
}
// Validation des codes pays (format ISO 3166-1 alpha-3)
if (grille.getOriginePays().length() != 3) {
throw new IllegalArgumentException("Le code pays d'origine doit être au format ISO 3166-1 alpha-3 (3 caractères)");
}
if (grille.getDestinationPays().length() != 3) {
throw new IllegalArgumentException("Le code pays de destination doit être au format ISO 3166-1 alpha-3 (3 caractères)");
}
}
@Override
public List<GrilleTarifaire> importerDepuisCsv(MultipartFile file, String mode) throws IOException {
log.info("Import CSV - Fichier: {}, Mode: {}", file.getOriginalFilename(), mode);
if ("REPLACE".equalsIgnoreCase(mode)) {
log.info("Mode REPLACE - Suppression de toutes les grilles existantes");
grilleTarifaireRepository.deleteAll();
}
List<GrilleTarifaire> grillesImportees = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), "UTF-8"))) {
String headerLine = reader.readLine();
if (headerLine == null) {
throw new IllegalArgumentException("Le fichier CSV est vide");
}
String[] headers = headerLine.split(",");
log.debug("En-têtes CSV: {}", Arrays.toString(headers));
String line;
int lineNumber = 1;
while ((line = reader.readLine()) != null) {
lineNumber++;
if (line.trim().isEmpty()) {
continue;
}
try {
GrilleTarifaire grille = parseCsvLine(line, headers, lineNumber);
if (grille != null) {
validerGrilleTarifaire(grille);
GrilleTarifaire grilleSauvegardee = grilleTarifaireRepository.save(grille);
grillesImportees.add(grilleSauvegardee);
}
} catch (Exception e) {
log.error("Erreur ligne {}: {}", lineNumber, e.getMessage());
throw new IllegalArgumentException("Erreur ligne " + lineNumber + ": " + e.getMessage());
}
}
}
log.info("Import CSV terminé - {} grilles importées", grillesImportees.size());
return grillesImportees;
}
@Override
public List<GrilleTarifaire> importerDepuisExcel(MultipartFile file, String sheetName, String mode) throws IOException {
log.info("Import Excel - Fichier: {}, Feuille: {}, Mode: {}", file.getOriginalFilename(), sheetName, mode);
// Pour cette version simplifiée, nous convertissons Excel vers CSV puis utilisons le parser CSV
// Dans une implémentation complète, nous utiliserions Apache POI
throw new UnsupportedOperationException("L'import Excel n'est pas encore implémenté. Utilisez le format CSV.");
}
@Override
public List<GrilleTarifaire> importerDepuisJson(List<GrilleTarifaire> grilles, String mode) {
log.info("Import JSON - {} grilles, Mode: {}", grilles.size(), mode);
if ("REPLACE".equalsIgnoreCase(mode)) {
log.info("Mode REPLACE - Suppression de toutes les grilles existantes");
grilleTarifaireRepository.deleteAll();
}
List<GrilleTarifaire> grillesImportees = new ArrayList<>();
for (int i = 0; i < grilles.size(); i++) {
try {
GrilleTarifaire grille = grilles.get(i);
validerGrilleTarifaire(grille);
// Si la grille a un ID et existe déjà, mise à jour. Sinon, création.
if (grille.getId() != null && grilleTarifaireRepository.existsById(grille.getId())) {
log.debug("Mise à jour grille existante ID: {}", grille.getId());
} else {
grille.setId(null); // Force la création d'une nouvelle grille
}
GrilleTarifaire grilleSauvegardee = grilleTarifaireRepository.save(grille);
grillesImportees.add(grilleSauvegardee);
} catch (Exception e) {
log.error("Erreur lors du traitement de la grille #{}: {}", i + 1, e.getMessage());
throw new IllegalArgumentException("Erreur grille #" + (i + 1) + ": " + e.getMessage());
}
}
log.info("Import JSON terminé - {} grilles importées", grillesImportees.size());
return grillesImportees;
}
@Override
public List<GrilleTarifaire> listerGrilles(int page, int size, String transporteur, String paysOrigine, String paysDestination) {
log.debug("Listing grilles - page: {}, size: {}, transporteur: {}, origine: {}, destination: {}",
page, size, transporteur, paysOrigine, paysDestination);
// Pour cette implémentation simplifiée, nous récupérons toutes les grilles et filtrons
// Dans une implémentation complète, nous utiliserions des requêtes JPA avec Specification
List<GrilleTarifaire> toutes = grilleTarifaireRepository.findAll();
// Application des filtres
return toutes.stream()
.filter(g -> transporteur == null || g.getTransporteur().toLowerCase().contains(transporteur.toLowerCase()))
.filter(g -> paysOrigine == null || g.getOriginePays().equalsIgnoreCase(paysOrigine))
.filter(g -> paysDestination == null || g.getDestinationPays().equalsIgnoreCase(paysDestination))
.skip((long) page * size)
.limit(size)
.collect(Collectors.toList());
}
@Override
public Map<String, Object> validerFichier(MultipartFile file) throws IOException {
log.info("Validation fichier - Nom: {}, Taille: {} bytes", file.getOriginalFilename(), file.getSize());
Map<String, Object> resultat = new HashMap<>();
List<String> erreurs = new ArrayList<>();
List<String> avertissements = new ArrayList<>();
int lignesValides = 0;
int lignesTotal = 0;
String filename = file.getOriginalFilename().toLowerCase();
resultat.put("nomFichier", file.getOriginalFilename());
resultat.put("tailleFichier", file.getSize());
resultat.put("typeFichier", filename.endsWith(".csv") ? "CSV" : filename.endsWith(".xlsx") ? "Excel" : "Inconnu");
if (filename.endsWith(".csv")) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), "UTF-8"))) {
String headerLine = reader.readLine();
lignesTotal++;
if (headerLine == null) {
erreurs.add("Le fichier est vide");
} else {
String[] headers = headerLine.split(",");
resultat.put("nombreColonnes", headers.length);
resultat.put("colonnes", Arrays.asList(headers));
// Vérification des colonnes obligatoires
List<String> colonnesObligatoires = Arrays.asList(
"nomGrille", "transporteur", "typeService", "originePays", "destinationPays",
"validiteDebut", "validiteFin"
);
for (String colonne : colonnesObligatoires) {
boolean trouve = false;
for (String header : headers) {
if (header.trim().equalsIgnoreCase(colonne)) {
trouve = true;
break;
}
}
if (!trouve) {
erreurs.add("Colonne obligatoire manquante: " + colonne);
}
}
String line;
int lineNumber = 1;
while ((line = reader.readLine()) != null && lineNumber <= 100) { // Limite pour la validation
lineNumber++;
lignesTotal++;
if (line.trim().isEmpty()) {
continue;
}
try {
GrilleTarifaire grille = parseCsvLine(line, headers, lineNumber);
if (grille != null) {
validerGrilleTarifaire(grille);
lignesValides++;
}
} catch (Exception e) {
erreurs.add("Ligne " + lineNumber + ": " + e.getMessage());
}
}
if (lineNumber > 100) {
avertissements.add("Validation limitée aux 100 premières lignes de données");
}
}
}
} else {
erreurs.add("Format de fichier non supporté. Seuls les fichiers CSV sont actuellement supportés.");
}
resultat.put("lignesTotal", lignesTotal);
resultat.put("lignesValides", lignesValides);
resultat.put("erreurs", erreurs);
resultat.put("avertissements", avertissements);
resultat.put("valide", erreurs.isEmpty());
return resultat;
}
private GrilleTarifaire parseCsvLine(String line, String[] headers, int lineNumber) {
String[] values = line.split(",", -1); // -1 pour conserver les valeurs vides
if (values.length != headers.length) {
throw new IllegalArgumentException("Nombre de colonnes incorrect. Attendu: " + headers.length + ", trouvé: " + values.length);
}
GrilleTarifaire grille = new GrilleTarifaire();
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < headers.length; i++) {
String header = headers[i].trim();
String value = values[i].trim();
if (value.isEmpty()) {
continue;
}
try {
switch (header.toLowerCase()) {
case "nomgrille":
grille.setNomGrille(value);
break;
case "transporteur":
grille.setTransporteur(value);
break;
case "typeservice":
grille.setTypeService(GrilleTarifaire.TypeService.valueOf(value.toUpperCase()));
break;
case "originepays":
grille.setOriginePays(value);
break;
case "destinationpays":
grille.setDestinationPays(value);
break;
case "origineville":
grille.setOrigineVille(value);
break;
case "destinationville":
grille.setDestinationVille(value);
break;
case "validitedebut":
grille.setValiditeDebut(LocalDate.parse(value, dateFormatter));
break;
case "validiteefin":
grille.setValiditeFin(LocalDate.parse(value, dateFormatter));
break;
case "incoterm":
grille.setIncoterm(value);
break;
case "modetransport":
grille.setModeTransport(GrilleTarifaire.ModeTransport.valueOf(value.toUpperCase()));
break;
case "actif":
grille.setActif(Boolean.parseBoolean(value));
break;
case "devisebase":
grille.setDeviseBase(value);
break;
case "commentaires":
grille.setCommentaires(value);
break;
// Pour les tarifs de fret et autres listes, nous aurions besoin d'un format plus complexe
// Dans cette implémentation simplifiée, nous les ignorons
default:
log.debug("Colonne ignorée: {}", header);
}
} catch (Exception e) {
throw new IllegalArgumentException("Erreur dans la colonne '" + header + "': " + e.getMessage());
}
}
return grille;
}
}

View File

@ -1,60 +1,7 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.dto.app.License;
import com.dh7789dev.xpeditis.dto.app.LicenseType;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.UUID;
@Service @Service
public class LicenseServiceImpl implements LicenseService { public class LicenseServiceImpl implements LicenseService {
private final LicenseRepository licenseRepository;
public LicenseServiceImpl(LicenseRepository licenseRepository) {
this.licenseRepository = licenseRepository;
}
@Override
public boolean validateLicense(UUID companyId) {
// TODO: Implement license validation logic
return true; // Temporary implementation
}
@Override
public boolean canAddUser(UUID companyId) {
// TODO: Implement user addition validation logic
return true; // Temporary implementation
}
@Override
public License createTrialLicense(Company company) {
// TODO: Implement trial license creation logic
throw new UnsupportedOperationException("Not implemented yet");
}
@Override
public License upgradeLicense(UUID companyId, LicenseType newType) {
// TODO: Implement license upgrade logic
throw new UnsupportedOperationException("Not implemented yet");
}
@Override
public void deactivateLicense(UUID licenseId) {
// TODO: Implement license deactivation logic
throw new UnsupportedOperationException("Not implemented yet");
}
@Override
public License getActiveLicense(UUID companyId) {
// TODO: Implement active license retrieval logic
throw new UnsupportedOperationException("Not implemented yet");
}
@Override
public long getDaysUntilExpiration(UUID companyId) {
// TODO: Implement days until expiration calculation logic
return Long.MAX_VALUE; // Temporary implementation
}
} }

View File

@ -1,13 +1,9 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest; import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.security.Principal; import java.security.Principal;
import java.util.Optional;
import java.util.UUID;
@Service @Service
public class UserServiceImpl implements UserService { public class UserServiceImpl implements UserService {
@ -22,67 +18,4 @@ public class UserServiceImpl implements UserService {
public void changePassword(ChangePasswordRequest request, Principal connectedUser) { public void changePassword(ChangePasswordRequest request, Principal connectedUser) {
userRepository.changePassword(request, connectedUser); userRepository.changePassword(request, connectedUser);
} }
@Override
public UserAccount createUser(RegisterRequest request) {
// Create UserAccount from RegisterRequest
UserAccount userAccount = UserAccount.builder()
.firstName(request.getFirstName())
.lastName(request.getLastName())
.email(new com.dh7789dev.xpeditis.dto.valueobject.Email(request.getEmail()))
.username(request.getUsername())
.phoneNumber(new com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber(request.getPhoneNumber()))
.authProvider(request.getAuthProvider())
.privacyPolicyAccepted(request.isPrivacyPolicyAccepted())
.isActive(true)
.build();
return userRepository.save(userAccount);
}
@Override
public UserAccount createGoogleUser(String googleToken) {
// TODO: Implement Google OAuth integration to extract user info from token
throw new UnsupportedOperationException("Google OAuth integration not implemented yet");
}
@Override
public Optional<UserAccount> findById(UUID id) {
return userRepository.findById(id);
}
@Override
public Optional<UserAccount> findByEmail(String email) {
return userRepository.findByEmail(email);
}
@Override
public Optional<UserAccount> findByUsername(String username) {
return userRepository.findByUsername(username);
}
@Override
public UserAccount updateProfile(UserAccount userAccount) {
return userRepository.save(userAccount);
}
@Override
public void deactivateUser(UUID userId) {
userRepository.deactivateUser(userId);
}
@Override
public void deleteUser(UUID userId) {
userRepository.deleteById(userId);
}
@Override
public boolean existsByEmail(String email) {
return userRepository.existsByEmail(email);
}
@Override
public boolean existsByUsername(String username) {
return userRepository.existsByUsername(username);
}
} }

View File

@ -1,503 +0,0 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
import com.dh7789dev.xpeditis.dto.app.GoogleUserInfo;
import com.dh7789dev.xpeditis.dto.app.Role;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
import com.dh7789dev.xpeditis.dto.valueobject.Email;
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
import com.dh7789dev.xpeditis.exception.AuthenticationException;
import com.dh7789dev.xpeditis.exception.BusinessException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("Authentication Service Tests")
class AuthenticationServiceImplTest {
@Mock
private AuthenticationRepository authenticationRepository;
@Mock
private UserRepository userRepository;
@Mock
private OAuth2Provider oAuth2Provider;
@Mock
private CompanyService companyService;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private AuthenticationServiceImpl authenticationService;
private AuthenticationRequest validAuthRequest;
private RegisterRequest validRegisterRequest;
private UserAccount testUserAccount;
private AuthenticationResponse testAuthResponse;
private GoogleUserInfo testGoogleUserInfo;
@BeforeEach
void setUp() {
validAuthRequest = new AuthenticationRequest("test@example.com", "password123");
validRegisterRequest = RegisterRequest.builder()
.firstName("John")
.lastName("Doe")
.email("john.doe@example.com")
.username("johndoe")
.password("password123")
.phoneNumber("+1234567890")
.privacyPolicyAccepted(true)
.build();
testUserAccount = UserAccount.builder()
.id(UUID.randomUUID())
.firstName("John")
.lastName("Doe")
.email(new Email("john.doe@example.com"))
.phoneNumber(new PhoneNumber("+1234567890"))
.username("johndoe")
.authProvider(AuthProvider.LOCAL)
.role(Role.USER)
.isActive(true)
.createdAt(LocalDateTime.now())
.build();
testAuthResponse = AuthenticationResponse.builder()
.accessToken("mock-jwt-token")
.refreshToken("mock-refresh-token")
.build();
testGoogleUserInfo = GoogleUserInfo.builder()
.id("google123")
.email("john.doe@example.com")
.firstName("John")
.lastName("Doe")
.verified(true)
.build();
}
@Nested
@DisplayName("Authentication Tests")
class AuthenticationTests {
@Test
@DisplayName("Should authenticate valid user successfully")
void shouldAuthenticateValidUser() {
// Given
when(authenticationRepository.authenticate(validAuthRequest)).thenReturn(testAuthResponse);
// When
AuthenticationResponse result = authenticationService.authenticate(validAuthRequest);
// Then
assertThat(result).isNotNull();
assertThat(result.getAccessToken()).isEqualTo("mock-jwt-token");
assertThat(result.getRefreshToken()).isEqualTo("mock-refresh-token");
verify(authenticationRepository).authenticate(validAuthRequest);
}
@Test
@DisplayName("Should throw exception when username is null")
void shouldThrowExceptionWhenUsernameIsNull() {
// Given
AuthenticationRequest invalidRequest = new AuthenticationRequest(null, "password123");
// When & Then
assertThatThrownBy(() -> authenticationService.authenticate(invalidRequest))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Username or email is required");
verify(authenticationRepository, never()).authenticate(any());
}
@Test
@DisplayName("Should throw exception when username is empty")
void shouldThrowExceptionWhenUsernameIsEmpty() {
// Given
AuthenticationRequest invalidRequest = new AuthenticationRequest(" ", "password123");
// When & Then
assertThatThrownBy(() -> authenticationService.authenticate(invalidRequest))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Username or email is required");
}
@Test
@DisplayName("Should throw exception when password is null")
void shouldThrowExceptionWhenPasswordIsNull() {
// Given
AuthenticationRequest invalidRequest = new AuthenticationRequest("test@example.com", null);
// When & Then
assertThatThrownBy(() -> authenticationService.authenticate(invalidRequest))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Password is required");
}
@Test
@DisplayName("Should throw exception when password is empty")
void shouldThrowExceptionWhenPasswordIsEmpty() {
// Given
AuthenticationRequest invalidRequest = new AuthenticationRequest("test@example.com", " ");
// When & Then
assertThatThrownBy(() -> authenticationService.authenticate(invalidRequest))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Password is required");
}
}
@Nested
@DisplayName("Registration Tests")
class RegistrationTests {
@Test
@DisplayName("Should register new user successfully")
void shouldRegisterNewUserSuccessfully() {
// Given
when(userRepository.existsByEmail("john.doe@example.com")).thenReturn(false);
when(userRepository.existsByUsername("johndoe")).thenReturn(false);
when(passwordEncoder.encode("password123")).thenReturn("encoded-password");
when(authenticationRepository.register(validRegisterRequest)).thenReturn(testAuthResponse);
// When
AuthenticationResponse result = authenticationService.register(validRegisterRequest);
// Then
assertThat(result).isNotNull();
assertThat(result.getAccessToken()).isEqualTo("mock-jwt-token");
verify(userRepository).existsByEmail("john.doe@example.com");
verify(userRepository).existsByUsername("johndoe");
verify(authenticationRepository).register(validRegisterRequest);
}
@Test
@DisplayName("Should throw exception when email already exists")
void shouldThrowExceptionWhenEmailAlreadyExists() {
// Given
when(userRepository.existsByEmail("john.doe@example.com")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> authenticationService.register(validRegisterRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("User with this email already exists");
verify(userRepository, never()).existsByUsername(any());
verify(authenticationRepository, never()).register(any());
}
@Test
@DisplayName("Should throw exception when username already exists")
void shouldThrowExceptionWhenUsernameAlreadyExists() {
// Given
when(userRepository.existsByEmail("john.doe@example.com")).thenReturn(false);
when(userRepository.existsByUsername("johndoe")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> authenticationService.register(validRegisterRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("Username already taken");
verify(authenticationRepository, never()).register(any());
}
@Test
@DisplayName("Should throw exception when email is null")
void shouldThrowExceptionWhenEmailIsNull() {
// Given
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
.email(null)
.build();
// When & Then
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("Email is required");
}
@Test
@DisplayName("Should throw exception when first name is null")
void shouldThrowExceptionWhenFirstNameIsNull() {
// Given
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
.firstName(null)
.build();
// When & Then
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("First name is required");
}
@Test
@DisplayName("Should throw exception when password is too short")
void shouldThrowExceptionWhenPasswordIsTooShort() {
// Given
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
.password("short")
.build();
// When & Then
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("Password must be at least 8 characters long");
}
@Test
@DisplayName("Should throw exception when email format is invalid")
void shouldThrowExceptionWhenEmailFormatIsInvalid() {
// Given
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
.email("invalid-email")
.build();
// When & Then
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("Invalid email format");
}
@Test
@DisplayName("Should throw exception when phone number format is invalid")
void shouldThrowExceptionWhenPhoneNumberFormatIsInvalid() {
// Given
RegisterRequest invalidRequest = validRegisterRequest.toBuilder()
.phoneNumber("invalid-phone")
.build();
// When & Then
assertThatThrownBy(() -> authenticationService.register(invalidRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("Invalid phone number format");
}
}
@Nested
@DisplayName("Google OAuth2 Authentication Tests")
class GoogleOAuth2Tests {
@Test
@DisplayName("Should authenticate with Google successfully for new user")
void shouldAuthenticateWithGoogleForNewUser() {
// Given
String googleToken = "valid-google-token";
when(oAuth2Provider.validateToken(googleToken)).thenReturn(true);
when(oAuth2Provider.getUserInfo(googleToken)).thenReturn(Optional.of(testGoogleUserInfo));
when(userRepository.findByGoogleId("google123")).thenReturn(Optional.empty());
when(userRepository.findByEmail("john.doe@example.com")).thenReturn(Optional.empty());
when(userRepository.save(any(UserAccount.class))).thenReturn(testUserAccount);
when(authenticationRepository.authenticateWithGoogle(any(UserAccount.class))).thenReturn(testAuthResponse);
// When
AuthenticationResponse result = authenticationService.authenticateWithGoogle(googleToken);
// Then
assertThat(result).isNotNull();
assertThat(result.getAccessToken()).isEqualTo("mock-jwt-token");
verify(oAuth2Provider).validateToken(googleToken);
verify(oAuth2Provider).getUserInfo(googleToken);
verify(userRepository).save(any(UserAccount.class));
verify(authenticationRepository).authenticateWithGoogle(any(UserAccount.class));
}
@Test
@DisplayName("Should authenticate with Google successfully for existing user")
void shouldAuthenticateWithGoogleForExistingUser() {
// Given
String googleToken = "valid-google-token";
when(oAuth2Provider.validateToken(googleToken)).thenReturn(true);
when(oAuth2Provider.getUserInfo(googleToken)).thenReturn(Optional.of(testGoogleUserInfo));
when(userRepository.findByGoogleId("google123")).thenReturn(Optional.of(testUserAccount));
when(userRepository.save(any(UserAccount.class))).thenReturn(testUserAccount);
when(authenticationRepository.authenticateWithGoogle(any(UserAccount.class))).thenReturn(testAuthResponse);
// When
AuthenticationResponse result = authenticationService.authenticateWithGoogle(googleToken);
// Then
assertThat(result).isNotNull();
verify(userRepository).findByGoogleId("google123");
verify(userRepository, never()).findByEmail(any());
verify(userRepository).save(testUserAccount);
}
@Test
@DisplayName("Should throw exception when Google token is null")
void shouldThrowExceptionWhenGoogleTokenIsNull() {
// When & Then
assertThatThrownBy(() -> authenticationService.authenticateWithGoogle(null))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Google token is required");
}
@Test
@DisplayName("Should throw exception when Google token is invalid")
void shouldThrowExceptionWhenGoogleTokenIsInvalid() {
// Given
String invalidToken = "invalid-token";
when(oAuth2Provider.validateToken(invalidToken)).thenReturn(false);
// When & Then
assertThatThrownBy(() -> authenticationService.authenticateWithGoogle(invalidToken))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Invalid Google token");
}
@Test
@DisplayName("Should throw exception when Google user info cannot be retrieved")
void shouldThrowExceptionWhenGoogleUserInfoCannotBeRetrieved() {
// Given
String validToken = "valid-token";
when(oAuth2Provider.validateToken(validToken)).thenReturn(true);
when(oAuth2Provider.getUserInfo(validToken)).thenReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> authenticationService.authenticateWithGoogle(validToken))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Failed to retrieve user information from Google");
}
}
@Nested
@DisplayName("Token Management Tests")
class TokenManagementTests {
@Test
@DisplayName("Should validate token successfully")
void shouldValidateTokenSuccessfully() {
// Given
String validToken = "valid-token";
when(authenticationRepository.validateToken(validToken)).thenReturn(true);
// When
boolean result = authenticationService.validateToken(validToken);
// Then
assertThat(result).isTrue();
verify(authenticationRepository).validateToken(validToken);
}
@Test
@DisplayName("Should return false for invalid token")
void shouldReturnFalseForInvalidToken() {
// Given
String invalidToken = "invalid-token";
when(authenticationRepository.validateToken(invalidToken)).thenReturn(false);
// When
boolean result = authenticationService.validateToken(invalidToken);
// Then
assertThat(result).isFalse();
}
@Test
@DisplayName("Should return false for null token")
void shouldReturnFalseForNullToken() {
// When
boolean result = authenticationService.validateToken(null);
// Then
assertThat(result).isFalse();
verify(authenticationRepository, never()).validateToken(any());
}
@Test
@DisplayName("Should refresh token successfully")
void shouldRefreshTokenSuccessfully() {
// Given
String refreshToken = "valid-refresh-token";
when(authenticationRepository.refreshToken(refreshToken)).thenReturn(testAuthResponse);
// When
AuthenticationResponse result = authenticationService.refreshToken(refreshToken);
// Then
assertThat(result).isNotNull();
verify(authenticationRepository).refreshToken(refreshToken);
}
@Test
@DisplayName("Should throw exception when refresh token is null")
void shouldThrowExceptionWhenRefreshTokenIsNull() {
// When & Then
assertThatThrownBy(() -> authenticationService.refreshToken(null))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Refresh token is required");
}
@Test
@DisplayName("Should logout successfully")
void shouldLogoutSuccessfully() {
// Given
String token = "valid-token";
// When
authenticationService.logout(token);
// Then
verify(authenticationRepository).logout(token);
}
@Test
@DisplayName("Should throw exception when logout token is null")
void shouldThrowExceptionWhenLogoutTokenIsNull() {
// When & Then
assertThatThrownBy(() -> authenticationService.logout(null))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Token is required");
}
}
@Nested
@DisplayName("User Retrieval Tests")
class UserRetrievalTests {
@Test
@DisplayName("Should get current user successfully")
void shouldGetCurrentUserSuccessfully() {
// Given
String token = "valid-token";
when(authenticationRepository.getCurrentUser(token)).thenReturn(testUserAccount);
// When
UserAccount result = authenticationService.getCurrentUser(token);
// Then
assertThat(result).isNotNull();
assertThat(result.getEmail().getValue()).isEqualTo("john.doe@example.com");
verify(authenticationRepository).getCurrentUser(token);
}
@Test
@DisplayName("Should throw exception when token is null for getCurrentUser")
void shouldThrowExceptionWhenTokenIsNullForGetCurrentUser() {
// When & Then
assertThatThrownBy(() -> authenticationService.getCurrentUser(null))
.isInstanceOf(AuthenticationException.class)
.hasMessage("Token is required");
}
}
}

View File

@ -1,273 +1,19 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.dto.app.License;
import com.dh7789dev.xpeditis.dto.app.LicenseType;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.CreateCompanyRequest;
import com.dh7789dev.xpeditis.dto.request.UpdateCompanyRequest;
import com.dh7789dev.xpeditis.exception.BusinessException;
import com.dh7789dev.xpeditis.exception.ResourceNotFoundException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime; import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@DisplayName("Company Service Tests")
class CompanyServiceImplTest { class CompanyServiceImplTest {
@Mock
private CompanyRepository companyRepository;
@Mock @Test
private LicenseRepository licenseRepository; void test(){
int test = 1 +1;
@Mock assertEquals(2,test);
private UserRepository userRepository;
@InjectMocks
private CompanyServiceImpl companyService;
private CreateCompanyRequest validCreateRequest;
private UpdateCompanyRequest validUpdateRequest;
private Company testCompany;
private License testLicense;
private UUID companyId;
@BeforeEach
void setUp() {
// Set configuration properties
ReflectionTestUtils.setField(companyService, "trialDurationDays", 30);
ReflectionTestUtils.setField(companyService, "trialMaxUsers", 5);
ReflectionTestUtils.setField(companyService, "basicMaxUsers", 50);
ReflectionTestUtils.setField(companyService, "premiumMaxUsers", 200);
ReflectionTestUtils.setField(companyService, "enterpriseMaxUsers", 1000);
companyId = UUID.randomUUID();
validCreateRequest = CreateCompanyRequest.builder()
.name("Test Company")
.description("A test company")
.website("https://testcompany.com")
.industry("Technology")
.build();
validUpdateRequest = UpdateCompanyRequest.builder()
.name("Updated Company")
.description("Updated description")
.website("https://updated.com")
.industry("Software")
.build();
testLicense = License.builder()
.id(UUID.randomUUID())
.type(LicenseType.TRIAL)
.issuedDate(LocalDateTime.now())
.expiryDate(LocalDateTime.now().plusDays(30))
.maxUsers(5)
.isActive(true)
.build();
testCompany = Company.builder()
.id(companyId)
.name("Test Company")
.description("A test company")
.website("https://testcompany.com")
.industry("Technology")
.isActive(true)
.createdAt(LocalDateTime.now())
.license(testLicense)
.build();
// Set bidirectional relationship
testLicense.setCompany(testCompany);
}
@Nested
@DisplayName("Company Creation Tests")
class CompanyCreationTests {
@Test
@DisplayName("Should create company successfully")
void shouldCreateCompanySuccessfully() {
// Given
when(companyRepository.existsByName("Test Company")).thenReturn(false);
when(companyRepository.save(any(Company.class))).thenReturn(testCompany);
when(licenseRepository.save(any(License.class))).thenReturn(testLicense);
// When
Company result = companyService.createCompany(validCreateRequest);
// Then
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("Test Company");
assertThat(result.getDescription()).isEqualTo("A test company");
assertThat(result.getWebsite()).isEqualTo("https://testcompany.com");
assertThat(result.getIndustry()).isEqualTo("Technology");
assertThat(result.isActive()).isTrue();
assertThat(result.getLicense()).isNotNull();
verify(companyRepository).existsByName("Test Company");
verify(companyRepository).save(any(Company.class));
verify(licenseRepository).save(any(License.class));
}
@Test
@DisplayName("Should throw exception when company name already exists")
void shouldThrowExceptionWhenCompanyNameAlreadyExists() {
// Given
when(companyRepository.existsByName("Test Company")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> companyService.createCompany(validCreateRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("Company with this name already exists");
verify(companyRepository, never()).save(any());
verify(licenseRepository, never()).save(any());
}
@Test
@DisplayName("Should throw exception when company name is null")
void shouldThrowExceptionWhenCompanyNameIsNull() {
// Given
CreateCompanyRequest invalidRequest = validCreateRequest.toBuilder()
.name(null)
.build();
// When & Then
assertThatThrownBy(() -> companyService.createCompany(invalidRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("Company name is required");
}
@Test
@DisplayName("Should throw exception when company name is too short")
void shouldThrowExceptionWhenCompanyNameIsTooShort() {
// Given
CreateCompanyRequest invalidRequest = validCreateRequest.toBuilder()
.name("A")
.build();
// When & Then
assertThatThrownBy(() -> companyService.createCompany(invalidRequest))
.isInstanceOf(BusinessException.class)
.hasMessage("Company name must be at least 2 characters long");
}
}
@Nested
@DisplayName("License Validation Tests")
class LicenseValidationTests {
@Test
@DisplayName("Should validate license successfully")
void shouldValidateLicenseSuccessfully() {
// Given
testLicense.setMaxUsers(10);
testLicense.setActive(true);
testLicense.setExpiryDate(LocalDateTime.now().plusDays(10));
testCompany.setLicense(testLicense);
when(companyRepository.findById(companyId)).thenReturn(Optional.of(testCompany));
// When
boolean result = companyService.validateLicense(companyId, 5);
// Then
assertThat(result).isTrue();
}
@Test
@DisplayName("Should return false when license is inactive")
void shouldReturnFalseWhenLicenseIsInactive() {
// Given
testLicense.setActive(false);
testCompany.setLicense(testLicense);
when(companyRepository.findById(companyId)).thenReturn(Optional.of(testCompany));
// When
boolean result = companyService.validateLicense(companyId, 5);
// Then
assertThat(result).isFalse();
}
@Test
@DisplayName("Should return false when requested users exceed license limit")
void shouldReturnFalseWhenRequestedUsersExceedLicenseLimit() {
// Given
testLicense.setMaxUsers(5);
testCompany.setLicense(testLicense);
when(companyRepository.findById(companyId)).thenReturn(Optional.of(testCompany));
// When
boolean result = companyService.validateLicense(companyId, 10);
// Then
assertThat(result).isFalse();
}
}
@Nested
@DisplayName("License Type Limits Tests")
class LicenseTypeLimitsTests {
@Test
@DisplayName("Should return correct max users for trial license")
void shouldReturnCorrectMaxUsersForTrialLicense() {
// When
int result = companyService.getMaxUsersForLicense(LicenseType.TRIAL);
// Then
assertThat(result).isEqualTo(5);
}
@Test
@DisplayName("Should return correct max users for basic license")
void shouldReturnCorrectMaxUsersForBasicLicense() {
// When
int result = companyService.getMaxUsersForLicense(LicenseType.BASIC);
// Then
assertThat(result).isEqualTo(50);
}
@Test
@DisplayName("Should return correct max users for premium license")
void shouldReturnCorrectMaxUsersForPremiumLicense() {
// When
int result = companyService.getMaxUsersForLicense(LicenseType.PREMIUM);
// Then
assertThat(result).isEqualTo(200);
}
@Test
@DisplayName("Should return correct max users for enterprise license")
void shouldReturnCorrectMaxUsersForEnterpriseLicense() {
// When
int result = companyService.getMaxUsersForLicense(LicenseType.ENTERPRISE);
// Then
assertThat(result).isEqualTo(1000);
}
} }
} }

View File

@ -1,277 +0,0 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@DisplayName("DevisCalculService - Tests unitaires")
class DevisCalculServiceImplTest {
@Mock
private GrilleTarifaireService grilleTarifaireService;
private DevisCalculServiceImpl devisCalculService;
private DemandeDevis demandeDevisValide;
private GrilleTarifaire grilleTarifaireStandard;
@BeforeEach
void setUp() {
devisCalculService = new DevisCalculServiceImpl(grilleTarifaireService);
// Créer une demande de devis valide pour les tests
demandeDevisValide = creerDemandeDevisValide();
// Créer une grille tarifaire standard pour les tests
grilleTarifaireStandard = creerGrilleTarifaireStandard();
}
@Test
@DisplayName("Doit calculer 3 offres avec succès")
void doitCalculerTroisOffresAvecSucces() {
// Given
when(grilleTarifaireService.trouverGrillesApplicables(any(DemandeDevis.class)))
.thenReturn(Arrays.asList(grilleTarifaireStandard));
// When
ReponseDevis reponse = devisCalculService.calculerTroisOffres(demandeDevisValide);
// Then
assertThat(reponse).isNotNull();
assertThat(reponse.getOffres()).hasSize(3);
// Vérifier que les 3 types d'offres sont présents
List<String> typesOffres = reponse.getOffres().stream()
.map(OffreCalculee::getType)
.toList();
assertThat(typesOffres).containsExactlyInAnyOrder("RAPIDE", "STANDARD", "ECONOMIQUE");
// Vérifier que l'offre rapide est la plus chère
OffreCalculee offreRapide = reponse.getOffres().stream()
.filter(o -> "RAPIDE".equals(o.getType()))
.findFirst().orElseThrow();
OffreCalculee offreStandard = reponse.getOffres().stream()
.filter(o -> "STANDARD".equals(o.getType()))
.findFirst().orElseThrow();
OffreCalculee offreEconomique = reponse.getOffres().stream()
.filter(o -> "ECONOMIQUE".equals(o.getType()))
.findFirst().orElseThrow();
assertThat(offreRapide.getPrixTotal()).isGreaterThan(offreStandard.getPrixTotal());
assertThat(offreStandard.getPrixTotal()).isGreaterThan(offreEconomique.getPrixTotal());
}
@Test
@DisplayName("Doit calculer correctement le colisage résumé")
void doitCalculerCorrectementColisageResume() {
// Given
when(grilleTarifaireService.trouverGrillesApplicables(any(DemandeDevis.class)))
.thenReturn(Arrays.asList(grilleTarifaireStandard));
DemandeDevis demande = creerDemandeAvecColisage();
// When
ReponseDevis reponse = devisCalculService.calculerTroisOffres(demande);
// Then
ReponseDevis.ColisageResume colisage = reponse.getColisageResume();
assertThat(colisage.getNombreColis()).isEqualTo(3); // 2 + 1
assertThat(colisage.getPoidsTotal()).isEqualTo(350.0); // (100*2) + (150*1)
assertThat(colisage.getVolumeTotal()).isEqualTo(0.35); // (0.1*2) + (0.15*1)
// Le poids taxable doit être le max entre poids réel et poids volumétrique
double poidsVolumetrique = 0.35 * 250; // 87.5 kg
assertThat(colisage.getPoidsTaxable()).isEqualTo(350.0); // Poids réel > poids volumétrique
}
@Test
@DisplayName("Doit lever une exception si aucune grille applicable")
void doitLeverExceptionSiAucuneGrilleApplicable() {
// Given
when(grilleTarifaireService.trouverGrillesApplicables(any(DemandeDevis.class)))
.thenReturn(Arrays.asList());
// When & Then
assertThatThrownBy(() -> devisCalculService.calculerTroisOffres(demandeDevisValide))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Aucune grille tarifaire applicable");
}
@Test
@DisplayName("Doit valider correctement une demande de devis valide")
void doitValiderCorrectementDemandeValide() {
// When & Then
assertThatCode(() -> devisCalculService.validerDemandeDevis(demandeDevisValide))
.doesNotThrowAnyException();
}
@Test
@DisplayName("Doit lever une exception si demande de devis nulle")
void doitLeverExceptionSiDemandeNulle() {
// When & Then
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("ne peut pas être nulle");
}
@Test
@DisplayName("Doit lever une exception si adresses manquantes")
void doitLeverExceptionSiAdressesManquantes() {
// Given
DemandeDevis demande = creerDemandeDevisValide();
demande.setDepart(null);
// When & Then
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(demande))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("adresses de départ et d'arrivée sont obligatoires");
}
@Test
@DisplayName("Doit lever une exception si aucun colisage")
void doitLeverExceptionSiAucunColisage() {
// Given
DemandeDevis demande = creerDemandeDevisValide();
demande.setColisages(Arrays.asList());
// When & Then
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(demande))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Au moins un colisage doit être défini");
}
@Test
@DisplayName("Doit lever une exception si poids invalide")
void doitLeverExceptionSiPoidsInvalide() {
// Given
DemandeDevis demande = creerDemandeDevisValide();
demande.getColisages().get(0).setPoids(0.0);
// When & Then
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(demande))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Le poids de chaque colisage doit être supérieur à 0");
}
// ================================
// Méthodes utilitaires pour créer les objets de test
// ================================
private DemandeDevis creerDemandeDevisValide() {
DemandeDevis demande = new DemandeDevis();
demande.setTypeService("EXPORT");
demande.setIncoterm("FOB");
demande.setTypeLivraison("Door to Door");
demande.setNomClient("Test Client");
demande.setEmailClient("test@example.com");
// Adresses
DemandeDevis.AdresseTransport depart = new DemandeDevis.AdresseTransport();
depart.setVille("Lyon");
depart.setCodePostal("69000");
depart.setPays("FRA");
demande.setDepart(depart);
DemandeDevis.AdresseTransport arrivee = new DemandeDevis.AdresseTransport();
arrivee.setVille("Shanghai");
arrivee.setCodePostal("200000");
arrivee.setPays("CHN");
demande.setArrivee(arrivee);
// Colisage simple
DemandeDevis.Colisage colisage = new DemandeDevis.Colisage();
colisage.setType(DemandeDevis.Colisage.TypeColisage.COLIS);
colisage.setQuantite(1);
colisage.setLongueur(50.0);
colisage.setLargeur(40.0);
colisage.setHauteur(30.0);
colisage.setPoids(25.0);
demande.setColisages(Arrays.asList(colisage));
demande.setDateEnlevement(LocalDate.now().plusDays(7));
return demande;
}
private DemandeDevis creerDemandeAvecColisage() {
DemandeDevis demande = creerDemandeDevisValide();
// Premier colisage
DemandeDevis.Colisage colisage1 = new DemandeDevis.Colisage();
colisage1.setType(DemandeDevis.Colisage.TypeColisage.COLIS);
colisage1.setQuantite(2);
colisage1.setLongueur(50.0);
colisage1.setLargeur(40.0);
colisage1.setHauteur(50.0); // Volume = 0.1 m³
colisage1.setPoids(100.0);
// Deuxième colisage
DemandeDevis.Colisage colisage2 = new DemandeDevis.Colisage();
colisage2.setType(DemandeDevis.Colisage.TypeColisage.PALETTE);
colisage2.setQuantite(1);
colisage2.setLongueur(120.0);
colisage2.setLargeur(80.0);
colisage2.setHauteur(150.0); // Volume = 0.15 m³
colisage2.setPoids(150.0);
colisage2.setGerbable(true);
demande.setColisages(Arrays.asList(colisage1, colisage2));
return demande;
}
private GrilleTarifaire creerGrilleTarifaireStandard() {
GrilleTarifaire grille = new GrilleTarifaire();
grille.setId(1L);
grille.setNomGrille("Test Grille Standard");
grille.setTransporteur("LESCHACO");
grille.setTypeService(GrilleTarifaire.TypeService.EXPORT);
grille.setOriginePays("FRA");
grille.setDestinationPays("CHN");
grille.setModeTransport(GrilleTarifaire.ModeTransport.MARITIME);
grille.setServiceType(GrilleTarifaire.ServiceType.STANDARD);
grille.setTransitTimeMin(25);
grille.setTransitTimeMax(30);
grille.setValiditeDebut(LocalDate.now().minusDays(30));
grille.setValiditeFin(LocalDate.now().plusDays(60));
grille.setDevise("EUR");
// Tarif de fret
TarifFret tarif = new TarifFret();
tarif.setPoidsMin(BigDecimal.ZERO);
tarif.setPoidsMax(BigDecimal.valueOf(1000));
tarif.setTauxUnitaire(BigDecimal.valueOf(2.5));
tarif.setUniteFacturation(TarifFret.UniteFacturation.KG);
tarif.setMinimumFacturation(BigDecimal.valueOf(100));
grille.setTarifsFret(Arrays.asList(tarif));
// Frais additionnels obligatoires
FraisAdditionnels fraisDoc = new FraisAdditionnels();
fraisDoc.setTypeFrais("DOCUMENTATION");
fraisDoc.setMontant(BigDecimal.valueOf(32));
fraisDoc.setUniteFacturation(FraisAdditionnels.UniteFacturation.LS);
fraisDoc.setObligatoire(true);
grille.setFraisAdditionnels(Arrays.asList(fraisDoc));
grille.setSurchargesDangereuses(Arrays.asList());
return grille;
}
}

View File

@ -1,187 +1,19 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
import com.dh7789dev.xpeditis.dto.request.UpdateProfileRequest;
import com.dh7789dev.xpeditis.dto.valueobject.Email;
import com.dh7789dev.xpeditis.dto.valueobject.PhoneNumber;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.security.Principal; import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@DisplayName("UserServiceImpl Tests")
class UserServiceImplTest { class UserServiceImplTest {
@Mock
private UserRepository userRepository;
@InjectMocks @Test
private UserServiceImpl userService; void test(){
int test = 1 +1;
private UserAccount testUserAccount; assertEquals(2,test);
private UpdateProfileRequest validUpdateProfileRequest;
private ChangePasswordRequest validChangePasswordRequest;
private RegisterRequest validRegisterRequest;
private Principal mockPrincipal;
@BeforeEach
void setUp() {
testUserAccount = UserAccount.builder()
.id(UUID.randomUUID())
.firstName("John")
.lastName("Doe")
.email(new Email("john.doe@example.com"))
.username("johndoe")
.phoneNumber(new PhoneNumber("+1234567890"))
.isActive(true)
.build();
validUpdateProfileRequest = UpdateProfileRequest.builder()
.firstName("John")
.lastName("Doe")
.phoneNumber("+1234567890")
.username("johndoe")
.build();
validChangePasswordRequest = new ChangePasswordRequest();
// Assuming ChangePasswordRequest has appropriate fields
mockPrincipal = java.security.Principal.class.cast(org.mockito.Mockito.mock(Principal.class));
validRegisterRequest = RegisterRequest.builder()
.firstName("John")
.lastName("Doe")
.email("john.doe@example.com")
.username("johndoe")
.password("Password123")
.confirmPassword("Password123")
.phoneNumber("+1234567890")
.companyName("Test Company")
.companyCountry("US")
.privacyPolicyAccepted(true)
.build();
}
@Nested
@DisplayName("Password Change Tests")
class PasswordChangeTests {
@Test
@DisplayName("Should delegate password change to repository")
void shouldDelegatePasswordChangeToRepository() {
// When
userService.changePassword(validChangePasswordRequest, mockPrincipal);
// Then
verify(userRepository).changePassword(validChangePasswordRequest, mockPrincipal);
}
}
@Nested
@DisplayName("User Existence Tests")
class UserExistenceTests {
@Test
@DisplayName("Should return false for existsByEmail (not implemented)")
void shouldReturnFalseForExistsByEmail() {
// When
boolean result = userService.existsByEmail("test@example.com");
// Then
assertThat(result).isFalse();
}
@Test
@DisplayName("Should return false for existsByUsername (not implemented)")
void shouldReturnFalseForExistsByUsername() {
// When
boolean result = userService.existsByUsername("testuser");
// Then
assertThat(result).isFalse();
}
}
@Nested
@DisplayName("Not Implemented Methods Tests")
class NotImplementedMethodsTests {
@Test
@DisplayName("Should throw UnsupportedOperationException for createUser")
void shouldThrowExceptionForCreateUser() {
assertThatThrownBy(() -> userService.createUser(validRegisterRequest))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessage("Not implemented yet");
}
@Test
@DisplayName("Should throw UnsupportedOperationException for createGoogleUser")
void shouldThrowExceptionForCreateGoogleUser() {
assertThatThrownBy(() -> userService.createGoogleUser("google-token"))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessage("Not implemented yet");
}
@Test
@DisplayName("Should throw UnsupportedOperationException for findById")
void shouldThrowExceptionForFindById() {
assertThatThrownBy(() -> userService.findById(UUID.randomUUID()))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessage("Not implemented yet");
}
@Test
@DisplayName("Should throw UnsupportedOperationException for findByEmail")
void shouldThrowExceptionForFindByEmail() {
assertThatThrownBy(() -> userService.findByEmail("test@example.com"))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessage("Not implemented yet");
}
@Test
@DisplayName("Should throw UnsupportedOperationException for findByUsername")
void shouldThrowExceptionForFindByUsername() {
assertThatThrownBy(() -> userService.findByUsername("testuser"))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessage("Not implemented yet");
}
@Test
@DisplayName("Should throw UnsupportedOperationException for updateProfile")
void shouldThrowExceptionForUpdateProfile() {
assertThatThrownBy(() -> userService.updateProfile(testUserAccount))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessage("Not implemented yet");
}
@Test
@DisplayName("Should throw UnsupportedOperationException for deactivateUser")
void shouldThrowExceptionForDeactivateUser() {
assertThatThrownBy(() -> userService.deactivateUser(UUID.randomUUID()))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessage("Not implemented yet");
}
@Test
@DisplayName("Should throw UnsupportedOperationException for deleteUser")
void shouldThrowExceptionForDeleteUser() {
assertThatThrownBy(() -> userService.deleteUser(UUID.randomUUID()))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessage("Not implemented yet");
}
} }
} }

View File

@ -1,6 +1,5 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest; import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse; import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
import com.dh7789dev.xpeditis.dto.request.RegisterRequest; import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
@ -9,9 +8,4 @@ public interface AuthenticationRepository {
AuthenticationResponse authenticate(AuthenticationRequest request); AuthenticationResponse authenticate(AuthenticationRequest request);
AuthenticationResponse register(RegisterRequest request); AuthenticationResponse register(RegisterRequest request);
AuthenticationResponse authenticateWithGoogle(UserAccount userAccount);
UserAccount getCurrentUser(String token);
void logout(String token);
boolean validateToken(String token);
AuthenticationResponse refreshToken(String refreshToken);
} }

View File

@ -1,22 +1,4 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.Company;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface CompanyRepository { public interface CompanyRepository {
Company save(Company company);
Optional<Company> findById(UUID id);
Optional<Company> findByName(String name);
List<Company> findAll();
boolean existsByName(String name);
void deleteById(UUID id);
} }

View File

@ -1,107 +0,0 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface GrilleTarifaireRepository {
/**
* Trouve toutes les grilles tarifaires valides pour une route et période donnée
*
* @param typeService IMPORT ou EXPORT
* @param originePays pays d'origine
* @param destinationPays pays de destination
* @param dateValidite date à laquelle la grille doit être valide
* @return liste des grilles correspondantes
*/
List<GrilleTarifaire> findGrillesApplicables(
String typeService,
String originePays,
String destinationPays,
LocalDate dateValidite
);
/**
* Trouve toutes les grilles tarifaires d'un type de service spécifique
*
* @param typeService IMPORT ou EXPORT
* @param serviceType RAPIDE, STANDARD ou ECONOMIQUE
* @param originePays pays d'origine
* @param destinationPays pays de destination
* @param dateValidite date de validité
* @return liste des grilles correspondantes
*/
List<GrilleTarifaire> findByServiceTypeAndRoute(
String typeService,
String serviceType,
String originePays,
String destinationPays,
LocalDate dateValidite
);
/**
* Sauvegarde une grille tarifaire
*
* @param grilleTarifaire la grille à sauvegarder
* @return la grille sauvegardée avec son ID généré
*/
GrilleTarifaire save(GrilleTarifaire grilleTarifaire);
/**
* Trouve une grille tarifaire par son ID
*
* @param id l'identifiant de la grille
* @return la grille trouvée ou Optional.empty()
*/
Optional<GrilleTarifaire> findById(Long id);
/**
* Supprime une grille tarifaire
*
* @param id l'identifiant de la grille à supprimer
*/
void deleteById(Long id);
/**
* Vérifie si une grille tarifaire existe
*
* @param id l'identifiant de la grille
* @return true si la grille existe
*/
boolean existsById(Long id);
/**
* Supprime toutes les grilles tarifaires
*/
void deleteAll();
/**
* Trouve toutes les grilles tarifaires avec pagination et filtres
*
* @param page numéro de page
* @param size taille de la page
* @param transporteur filtre par transporteur (optionnel)
* @param paysOrigine filtre par pays d'origine (optionnel)
* @param paysDestination filtre par pays de destination (optionnel)
* @return liste des grilles correspondantes
*/
List<GrilleTarifaire> findAllWithFilters(int page, int size, String transporteur, String paysOrigine, String paysDestination);
/**
* Sauvegarde une liste de grilles tarifaires
*
* @param grilles la liste des grilles à sauvegarder
* @return la liste des grilles sauvegardées
*/
List<GrilleTarifaire> saveAll(List<GrilleTarifaire> grilles);
/**
* Trouve toutes les grilles tarifaires
*
* @return la liste de toutes les grilles
*/
List<GrilleTarifaire> findAll();
}

View File

@ -1,24 +1,4 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.License;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface LicenseRepository { public interface LicenseRepository {
License save(License license);
Optional<License> findById(UUID id);
Optional<License> findActiveLicenseByCompanyId(UUID companyId);
List<License> findByCompanyId(UUID companyId);
Optional<License> findByLicenseKey(String licenseKey);
void deleteById(UUID id);
void deactivateLicense(UUID id);
} }

View File

@ -1,11 +0,0 @@
package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.GoogleUserInfo;
import java.util.Optional;
public interface OAuth2Provider {
boolean validateToken(String accessToken);
Optional<GoogleUserInfo> getUserInfo(String accessToken);
}

View File

@ -1,34 +1,10 @@
package com.dh7789dev.xpeditis; package com.dh7789dev.xpeditis;
import com.dh7789dev.xpeditis.dto.app.UserAccount;
import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest; import com.dh7789dev.xpeditis.dto.request.ChangePasswordRequest;
import java.security.Principal; import java.security.Principal;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface UserRepository { public interface UserRepository {
void changePassword(ChangePasswordRequest request, Principal connectedUser); void changePassword(ChangePasswordRequest request, Principal connectedUser);
UserAccount save(UserAccount userAccount);
Optional<UserAccount> findById(UUID id);
Optional<UserAccount> findByEmail(String email);
Optional<UserAccount> findByUsername(String username);
Optional<UserAccount> findByGoogleId(String googleId);
boolean existsByEmail(String email);
boolean existsByUsername(String username);
void deleteById(UUID id);
void deactivateUser(UUID id);
List<UserAccount> findByCompanyIdAndIsActive(UUID companyId, boolean isActive);
} }

View File

@ -32,11 +32,10 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!-- MapStruct disabled - using manual mappers --> <dependency>
<!-- <dependency>
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId> <artifactId>mapstruct</artifactId>
</dependency> --> </dependency>
<!-- spring-boot dependencies --> <!-- spring-boot dependencies -->
<dependency> <dependency>
@ -129,8 +128,7 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version> <version>${org.projectlombok.version}</version>
</path> </path>
<!-- MapStruct processors disabled - using manual mappers --> <path>
<!-- <path>
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId> <artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version> <version>${org.mapstruct.version}</version>
@ -139,7 +137,7 @@
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId> <artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version> <version>0.2.0</version>
</path> --> </path>
</annotationProcessorPaths> </annotationProcessorPaths>
</configuration> </configuration>
</plugin> </plugin>

View File

@ -2,15 +2,7 @@ package com.dh7789dev.xpeditis.dao;
import com.dh7789dev.xpeditis.entity.CompanyEntity; import com.dh7789dev.xpeditis.entity.CompanyEntity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository public interface CompanyDao extends JpaRepository<CompanyEntity, Long> {
public interface CompanyDao extends JpaRepository<CompanyEntity, UUID> {
Optional<CompanyEntity> findByName(String name);
boolean existsByName(String name);
} }

View File

@ -1,165 +0,0 @@
package com.dh7789dev.xpeditis.dao;
import com.dh7789dev.xpeditis.GrilleTarifaireRepository;
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
import com.dh7789dev.xpeditis.entity.GrilleTarifaireEntity;
import com.dh7789dev.xpeditis.mapper.GrilleTarifaireMapper;
import com.dh7789dev.xpeditis.repository.GrilleTarifaireJpaRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
@Slf4j
public class GrilleTarifaireDao implements GrilleTarifaireRepository {
private final GrilleTarifaireJpaRepository jpaRepository;
private final GrilleTarifaireMapper mapper;
@Override
public List<GrilleTarifaire> findGrillesApplicables(
String typeService,
String originePays,
String destinationPays,
LocalDate dateValidite) {
log.debug("Recherche des grilles applicables: {} {} -> {} à la date {}",
typeService, originePays, destinationPays, dateValidite);
GrilleTarifaireEntity.TypeService typeServiceEnum = parseTypeService(typeService);
List<GrilleTarifaireEntity> entities = jpaRepository.findGrillesApplicables(
typeServiceEnum, originePays, destinationPays, dateValidite);
log.debug("Trouvé {} grilles dans la base de données", entities.size());
return entities.stream()
.map(mapper::entityToDto)
.collect(Collectors.toList());
}
@Override
public List<GrilleTarifaire> findByServiceTypeAndRoute(
String typeService,
String serviceType,
String originePays,
String destinationPays,
LocalDate dateValidite) {
GrilleTarifaireEntity.TypeService typeServiceEnum = parseTypeService(typeService);
GrilleTarifaireEntity.ServiceType serviceTypeEnum = parseServiceType(serviceType);
List<GrilleTarifaireEntity> entities = jpaRepository.findByServiceTypeAndRoute(
typeServiceEnum, serviceTypeEnum, originePays, destinationPays, dateValidite);
return entities.stream()
.map(mapper::entityToDto)
.collect(Collectors.toList());
}
@Override
public GrilleTarifaire save(GrilleTarifaire grilleTarifaire) {
log.debug("Sauvegarde de la grille tarifaire: {}", grilleTarifaire.getNomGrille());
GrilleTarifaireEntity entity = mapper.dtoToEntity(grilleTarifaire);
GrilleTarifaireEntity savedEntity = jpaRepository.save(entity);
return mapper.entityToDto(savedEntity);
}
@Override
public Optional<GrilleTarifaire> findById(Long id) {
log.debug("Recherche de la grille tarifaire par ID: {}", id);
return jpaRepository.findById(id)
.map(mapper::entityToDto);
}
@Override
public void deleteById(Long id) {
log.info("Suppression de la grille tarifaire avec l'ID: {}", id);
jpaRepository.deleteById(id);
}
@Override
public boolean existsById(Long id) {
return jpaRepository.existsById(id);
}
@Override
public void deleteAll() {
log.info("Suppression de toutes les grilles tarifaires");
jpaRepository.deleteAll();
}
@Override
public List<GrilleTarifaire> findAllWithFilters(int page, int size, String transporteur, String paysOrigine, String paysDestination) {
log.debug("Recherche des grilles avec filtres - page: {}, size: {}, transporteur: {}, origine: {}, destination: {}",
page, size, transporteur, paysOrigine, paysDestination);
// Simple implementation - for more complex filtering, we would use Spring Data specifications
List<GrilleTarifaireEntity> entities = jpaRepository.findAll();
return entities.stream()
.filter(entity -> transporteur == null || transporteur.isEmpty() ||
entity.getTransporteur().equalsIgnoreCase(transporteur))
.filter(entity -> paysOrigine == null || paysOrigine.isEmpty() ||
entity.getOriginePays().equalsIgnoreCase(paysOrigine))
.filter(entity -> paysDestination == null || paysDestination.isEmpty() ||
entity.getDestinationPays().equalsIgnoreCase(paysDestination))
.skip(page * size)
.limit(size)
.map(mapper::entityToDto)
.collect(Collectors.toList());
}
@Override
public List<GrilleTarifaire> saveAll(List<GrilleTarifaire> grilles) {
log.info("Sauvegarde de {} grilles tarifaires", grilles.size());
List<GrilleTarifaireEntity> entities = grilles.stream()
.map(mapper::dtoToEntity)
.collect(Collectors.toList());
List<GrilleTarifaireEntity> savedEntities = jpaRepository.saveAll(entities);
return savedEntities.stream()
.map(mapper::entityToDto)
.collect(Collectors.toList());
}
@Override
public List<GrilleTarifaire> findAll() {
log.debug("Recherche de toutes les grilles tarifaires");
List<GrilleTarifaireEntity> entities = jpaRepository.findAll();
return entities.stream()
.map(mapper::entityToDto)
.collect(Collectors.toList());
}
private GrilleTarifaireEntity.TypeService parseTypeService(String typeService) {
try {
return GrilleTarifaireEntity.TypeService.valueOf(typeService.toUpperCase());
} catch (IllegalArgumentException e) {
log.error("Type de service invalide: {}", typeService);
throw new IllegalArgumentException("Type de service invalide: " + typeService);
}
}
private GrilleTarifaireEntity.ServiceType parseServiceType(String serviceType) {
try {
return GrilleTarifaireEntity.ServiceType.valueOf(serviceType.toUpperCase());
} catch (IllegalArgumentException e) {
log.error("Service type invalide: {}", serviceType);
throw new IllegalArgumentException("Service type invalide: " + serviceType);
}
}
}

View File

@ -2,21 +2,6 @@ package com.dh7789dev.xpeditis.dao;
import com.dh7789dev.xpeditis.entity.LicenseEntity; import com.dh7789dev.xpeditis.entity.LicenseEntity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List; public interface LicenseDao extends JpaRepository<LicenseEntity, Long> {
import java.util.Optional;
import java.util.UUID;
@Repository
public interface LicenseDao extends JpaRepository<LicenseEntity, UUID> {
Optional<LicenseEntity> findByLicenseKey(String licenseKey);
List<LicenseEntity> findByCompanyId(UUID companyId);
@Query("SELECT l FROM LicenseEntity l WHERE l.company.id = :companyId AND l.isActive = true AND (l.expirationDate IS NULL OR l.expirationDate > CURRENT_DATE)")
Optional<LicenseEntity> findActiveLicenseByCompanyId(@Param("companyId") UUID companyId);
} }

View File

@ -6,7 +6,6 @@ import org.springframework.data.jpa.repository.Query;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@ -20,5 +19,5 @@ public interface TokenDao extends JpaRepository<TokenEntity, Integer> {
on t.user.id = u.id\s on t.user.id = u.id\s
where u.id = :userId and (t.expired = false or t.revoked = false)\s where u.id = :userId and (t.expired = false or t.revoked = false)\s
""") """)
List<TokenEntity> findAllValidTokenByUserId(UUID userId); List<TokenEntity> findAllValidTokenByUserId(String userId);
} }

View File

@ -2,21 +2,16 @@ package com.dh7789dev.xpeditis.dao;
import com.dh7789dev.xpeditis.entity.UserEntity; import com.dh7789dev.xpeditis.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
@Repository import org.springframework.data.jpa.repository.Query;
public interface UserDao extends JpaRepository<UserEntity, UUID> { import org.springframework.stereotype.Repository;
public interface UserDao extends JpaRepository<UserEntity, Long> {
@Query("SELECT u FROM UserEntity u WHERE u.username = :username")
Optional<UserEntity> findByUsername(String username); Optional<UserEntity> findByUsername(String username);
Optional<UserEntity> findByEmail(String email);
Optional<UserEntity> findByGoogleId(String googleId);
boolean existsByUsername(String username); boolean existsByUsername(String username);
boolean existsByEmail(String email);
} }

View File

@ -1,6 +0,0 @@
package com.dh7789dev.xpeditis.entity;
public enum AuthProviderEntity {
LOCAL,
GOOGLE
}

View File

@ -10,102 +10,58 @@ import lombok.experimental.FieldNameConstants;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.UUID;
@Entity @Entity
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults( level = AccessLevel.PRIVATE)
@Table(name = "companies") @Table(name = "Company")
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class) public class CompanyEntity extends BaseEntity {
public class CompanyEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.UUID) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BINARY(16)") private Long id;
UUID id;
@Column(name = "name", nullable = false, unique = true, length = 100) @Column(name = "name", length = 50)
String name; private String name;
@Column(name = "country", length = 50) @Column(name = "country", length = 50)
String country; private String country;
@Column(name = "siren", length = 20) @Column(name = "siren")
String siren; private String siren;
@Column(name = "num_eori", length = 50) @Column(name = "num_eori")
String numEori; private String num_eori;
@Column(name = "phone", length = 20) @Column(name = "phone", length = 20)
String phone; private String phone;
@Column(name = "is_active", nullable = false) @OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
boolean isActive = true; private List<UserEntity> users;
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
List<UserEntity> users; private List<QuoteEntity> quotes;
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY) @OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
List<LicenseEntity> licenses; private List<ExportFolderEntity> exports;
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
List<QuoteEntity> quotes;
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
List<ExportFolderEntity> exports;
@Column(name = "created_at", updatable = false) @Column(name = "created_at", updatable = false)
LocalDateTime createdAt; private LocalDateTime createdAt;
@Column(name = "updated_at") @Column(name = "modified_at")
LocalDateTime updatedAt; private LocalDateTime modifiedAt;
@org.springframework.data.annotation.CreatedDate
@Column(name = "created_date", updatable = false, nullable = false)
java.time.Instant createdDate;
@org.springframework.data.annotation.LastModifiedDate
@Column(name = "modified_date", nullable = false)
java.time.Instant modifiedDate;
@org.springframework.data.annotation.CreatedBy
@Column(name = "created_by", updatable = false, nullable = false)
String createdBy = "SYSTEM";
@org.springframework.data.annotation.LastModifiedBy
@Column(name = "modified_by")
String modifiedBy;
@PrePersist @PrePersist
public void onCreate() { public void onCreate() {
if (id == null) {
id = UUID.randomUUID();
}
createdAt = LocalDateTime.now(); createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now(); modifiedAt = LocalDateTime.now();
} }
@PreUpdate @PreUpdate
public void onUpdate() { public void onUpdate() {
updatedAt = LocalDateTime.now(); modifiedAt = LocalDateTime.now();
}
public int getActiveUserCount() {
return users != null ? (int) users.stream()
.filter(UserEntity::isActive)
.count() : 0;
}
public LicenseEntity getActiveLicense() {
return licenses != null ? licenses.stream()
.filter(LicenseEntity::isActive)
.filter(license -> license.getExpirationDate() == null ||
license.getExpirationDate().isAfter(java.time.LocalDate.now()))
.findFirst()
.orElse(null) : null;
} }
} }

View File

@ -1,54 +0,0 @@
package com.dh7789dev.xpeditis.entity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import java.math.BigDecimal;
@Entity
@Getter
@Setter
@NoArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@Table(name = "frais_additionnels",
indexes = {
@Index(name = "idx_grille_type", columnList = "grille_id, type_frais")
})
public class FraisAdditionnelsEntity extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "grille_id", nullable = false)
private GrilleTarifaireEntity grille;
@Column(name = "type_frais", nullable = false, length = 50)
private String typeFrais;
@Column(name = "description", length = 200)
private String description;
@Column(name = "montant", nullable = false, precision = 10, scale = 2)
private BigDecimal montant;
@Enumerated(EnumType.STRING)
@Column(name = "unite_facturation", nullable = false)
private UniteFacturation uniteFacturation;
@Column(name = "montant_minimum", precision = 10, scale = 2)
private BigDecimal montantMinimum;
@Column(name = "obligatoire")
private Boolean obligatoire = false;
@Column(name = "applicable_marchandise_dangereuse")
private Boolean applicableMarchandiseDangereuse = false;
public enum UniteFacturation {
LS, KG, M3, PALETTE, POURCENTAGE
}
}

View File

@ -1,109 +0,0 @@
package com.dh7789dev.xpeditis.entity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@NoArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@Table(name = "grilles_tarifaires")
public class GrilleTarifaireEntity extends BaseEntity {
@Column(name = "nom_grille", nullable = false, length = 100)
private String nomGrille;
@Column(name = "transporteur", nullable = false, length = 50)
private String transporteur;
@Enumerated(EnumType.STRING)
@Column(name = "type_service", nullable = false)
private TypeService typeService;
@Column(name = "origine_pays", nullable = false, length = 3)
private String originePays;
@Column(name = "origine_ville", length = 100)
private String origineVille;
@Column(name = "origine_port_code", length = 10)
private String originePortCode;
@Column(name = "destination_pays", nullable = false, length = 3)
private String destinationPays;
@Column(name = "destination_ville", length = 100)
private String destinationVille;
@Column(name = "destination_port_code", length = 10)
private String destinationPortCode;
@Column(name = "incoterm", length = 10)
private String incoterm;
@Enumerated(EnumType.STRING)
@Column(name = "mode_transport")
private ModeTransport modeTransport;
@Enumerated(EnumType.STRING)
@Column(name = "service_type")
private ServiceType serviceType;
@Column(name = "transit_time_min")
private Integer transitTimeMin;
@Column(name = "transit_time_max")
private Integer transitTimeMax;
@Column(name = "validite_debut", nullable = false)
private LocalDate validiteDebut;
@Column(name = "validite_fin", nullable = false)
private LocalDate validiteFin;
@Column(name = "devise", length = 3)
private String devise = "EUR";
@Column(name = "actif")
private Boolean actif = true;
@Column(name = "devise_base", length = 3)
private String deviseBase;
@Column(name = "commentaires", columnDefinition = "TEXT")
private String commentaires;
@OneToMany(mappedBy = "grille", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<TarifFretEntity> tarifsFret = new ArrayList<>();
@OneToMany(mappedBy = "grille", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<FraisAdditionnelsEntity> fraisAdditionnels = new ArrayList<>();
@OneToMany(mappedBy = "grille", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<SurchargeDangereuse> surchargesDangereuses = new ArrayList<>();
public enum TypeService {
IMPORT, EXPORT
}
public enum ModeTransport {
MARITIME, AERIEN, ROUTIER, FERROVIAIRE
}
public enum ServiceType {
RAPIDE, STANDARD, ECONOMIQUE
}
}

View File

@ -10,100 +10,46 @@ import lombok.experimental.FieldNameConstants;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID;
@Entity @Entity
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults( level = AccessLevel.PRIVATE)
@Table(name = "licenses") @Table(name = "License")
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class) public class LicenseEntity extends BaseEntity {
public class LicenseEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.UUID) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BINARY(16)") private Long id;
UUID id;
@Column(name = "license_key", unique = true, nullable = false) @Column(unique = true)
String licenseKey; private String licenseKey;
@Enumerated(EnumType.STRING) @Column(name = "expirationDate")
@Column(name = "type", nullable = false) private LocalDate expirationDate;
LicenseTypeEntity type;
@Column(name = "start_date", nullable = false) private boolean active;
LocalDate startDate;
@Column(name = "expiration_date") @OneToOne
LocalDate expirationDate; @JoinColumn(name = "user_id", unique = true)
private UserEntity user;
@Column(name = "max_users", nullable = false)
int maxUsers;
@Column(name = "is_active", nullable = false)
boolean isActive = true;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id", nullable = false)
CompanyEntity company;
@Column(name = "created_at", updatable = false) @Column(name = "created_at", updatable = false)
LocalDateTime createdAt; private LocalDateTime createdAt;
@org.springframework.data.annotation.CreatedDate @Column(name = "modified_at")
@Column(name = "created_date", updatable = false, nullable = false) private LocalDateTime modifiedAt;
java.time.Instant createdDate;
@org.springframework.data.annotation.LastModifiedDate
@Column(name = "modified_date", nullable = false)
java.time.Instant modifiedDate;
@org.springframework.data.annotation.CreatedBy
@Column(name = "created_by", updatable = false, nullable = false)
String createdBy = "SYSTEM";
@org.springframework.data.annotation.LastModifiedBy
@Column(name = "modified_by")
String modifiedBy;
@PrePersist @PrePersist
public void onCreate() { public void onCreate() {
if (id == null) {
id = UUID.randomUUID();
}
if (licenseKey == null) {
licenseKey = generateLicenseKey();
}
createdAt = LocalDateTime.now(); createdAt = LocalDateTime.now();
modifiedAt = LocalDateTime.now();
} }
public boolean isExpired() { @PreUpdate
return expirationDate != null && expirationDate.isBefore(LocalDate.now()); public void onUpdate() {
} modifiedAt = LocalDateTime.now();
public boolean isValid() {
return isActive && !isExpired();
}
public boolean canAddUser(int currentUserCount) {
return !hasUserLimit() || currentUserCount < maxUsers;
}
public boolean hasUserLimit() {
return type != null && type.hasUserLimit();
}
public long getDaysUntilExpiration() {
return expirationDate != null ?
java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(), expirationDate) :
Long.MAX_VALUE;
}
private String generateLicenseKey() {
return "LIC-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
} }
} }

View File

@ -1,32 +0,0 @@
package com.dh7789dev.xpeditis.entity;
public enum LicenseTypeEntity {
TRIAL(5, 30),
BASIC(10, -1),
PREMIUM(50, -1),
ENTERPRISE(-1, -1);
private final int maxUsers;
private final int durationDays;
LicenseTypeEntity(int maxUsers, int durationDays) {
this.maxUsers = maxUsers;
this.durationDays = durationDays;
}
public int getMaxUsers() {
return maxUsers;
}
public int getDurationDays() {
return durationDays;
}
public boolean hasUserLimit() {
return maxUsers > 0;
}
public boolean hasTimeLimit() {
return durationDays > 0;
}
}

View File

@ -1,48 +0,0 @@
package com.dh7789dev.xpeditis.entity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import java.math.BigDecimal;
@Entity
@Getter
@Setter
@NoArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@Table(name = "surcharges_dangereuses",
indexes = {
@Index(name = "idx_grille_classe", columnList = "grille_id, classe_adr")
})
public class SurchargeDangereuse extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "grille_id", nullable = false)
private GrilleTarifaireEntity grille;
@Column(name = "classe_adr", length = 10)
private String classeAdr;
@Column(name = "un_number", length = 10)
private String unNumber;
@Column(name = "surcharge", nullable = false, precision = 10, scale = 2)
private BigDecimal surcharge;
@Enumerated(EnumType.STRING)
@Column(name = "unite_facturation", nullable = false)
private UniteFacturation uniteFacturation;
@Column(name = "minimum", precision = 10, scale = 2)
private BigDecimal minimum;
public enum UniteFacturation {
LS, KG, COLIS
}
}

View File

@ -1,55 +0,0 @@
package com.dh7789dev.xpeditis.entity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import java.math.BigDecimal;
@Entity
@Getter
@Setter
@NoArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@Table(name = "tarifs_fret",
indexes = {
@Index(name = "idx_grille_poids", columnList = "grille_id, poids_min, poids_max"),
@Index(name = "idx_grille_volume", columnList = "grille_id, volume_min, volume_max")
})
public class TarifFretEntity extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "grille_id", nullable = false)
private GrilleTarifaireEntity grille;
@Column(name = "poids_min", precision = 10, scale = 2)
private BigDecimal poidsMin;
@Column(name = "poids_max", precision = 10, scale = 2)
private BigDecimal poidsMax;
@Column(name = "volume_min", precision = 10, scale = 3)
private BigDecimal volumeMin;
@Column(name = "volume_max", precision = 10, scale = 3)
private BigDecimal volumeMax;
@Column(name = "taux_unitaire", nullable = false, precision = 10, scale = 2)
private BigDecimal tauxUnitaire;
@Enumerated(EnumType.STRING)
@Column(name = "unite_facturation", nullable = false)
private UniteFacturation uniteFacturation;
@Column(name = "minimum_facturation", precision = 10, scale = 2)
private BigDecimal minimumFacturation;
public enum UniteFacturation {
KG, M3, PALETTE, COLIS, LS
}
}

View File

@ -1,6 +1,5 @@
package com.dh7789dev.xpeditis.entity; package com.dh7789dev.xpeditis.entity;
import com.dh7789dev.xpeditis.dto.app.Role;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
@ -8,143 +7,91 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import org.hibernate.annotations.NaturalId;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.UUID;
@Entity @Entity
@Getter @Getter
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults( level = AccessLevel.PRIVATE)
@Table(name = "users") @Table(name = "Users")
@EntityListeners(org.springframework.data.jpa.domain.support.AuditingEntityListener.class) public class UserEntity extends BaseEntity implements UserDetails {
public class UserEntity implements UserDetails {
@Id @NaturalId
@GeneratedValue(strategy = GenerationType.UUID) @Column(nullable = false, unique = true, length = 50)
@Column(columnDefinition = "BINARY(16)") private String username;
UUID id;
@Column(name = "first_name", nullable = false, length = 50) @Column(name = "first_name", length = 50)
String firstName; private String firstName;
@Column(name = "last_name", nullable = false, length = 50) @Column(name = "last_name", length = 50)
String lastName; private String lastName;
@Column(nullable = false, unique = true) @Column(unique = true, nullable = false)
String email; private String email;
@Column(name = "username", unique = true, length = 50) @Column(nullable = false)
String username; private String password;
@Column(name = "password") @Column(name = "phone", length = 20)
String password; private String phone;
@Column(name = "phone_number", nullable = true, length = 20)
String phoneNumber;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(name = "auth_provider", nullable = false) @Column(nullable = false)
AuthProviderEntity authProvider = AuthProviderEntity.LOCAL; private Role role;
@Column(name = "google_id") @Column(name = "enabled", nullable = false, columnDefinition = "BOOLEAN DEFAULT TRUE NOT NULL")
String googleId; private boolean enabled;
@Column(name = "privacy_policy_accepted", nullable = false) @ManyToOne
boolean privacyPolicyAccepted = false; private CompanyEntity company;
@Column(name = "privacy_policy_accepted_at") @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
LocalDateTime privacyPolicyAcceptedAt; private LicenseEntity license;
@Column(name = "last_login_at") @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
LocalDateTime lastLoginAt; private List<QuoteEntity> quotes;
@Column(name = "is_active", nullable = false) @OneToMany(mappedBy = "user")
boolean isActive = true; private List<TokenEntity> tokens;
@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false)
Role role = Role.USER;
@Column(name = "enabled", nullable = false)
boolean enabled = true;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id")
CompanyEntity company;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
List<QuoteEntity> quotes;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
List<TokenEntity> tokens;
@Column(name = "created_at", updatable = false) @Column(name = "created_at", updatable = false)
LocalDateTime createdAt; private LocalDateTime createdAt;
@Column(name = "updated_at") @Column(name = "modified_at")
LocalDateTime updatedAt; private LocalDateTime modifiedAt;
@org.springframework.data.annotation.CreatedDate
@Column(name = "created_date", updatable = false, nullable = false)
java.time.Instant createdDate;
@org.springframework.data.annotation.LastModifiedDate
@Column(name = "modified_date", nullable = false)
java.time.Instant modifiedDate;
@org.springframework.data.annotation.CreatedBy
@Column(name = "created_by", updatable = false, nullable = false)
String createdBy = "SYSTEM";
@org.springframework.data.annotation.LastModifiedBy
@Column(name = "modified_by")
String modifiedBy;
@PrePersist @PrePersist
public void onCreate() { public void onCreate() {
if (id == null) {
id = UUID.randomUUID();
}
createdAt = LocalDateTime.now(); createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now(); modifiedAt = LocalDateTime.now();
} }
@PreUpdate @PreUpdate
public void onUpdate() { public void onUpdate() {
updatedAt = LocalDateTime.now(); modifiedAt = LocalDateTime.now();
} }
@Override @Override
public Collection<? extends GrantedAuthority> getAuthorities() { public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); return role.getAuthorities();
}
@Override
public String getUsername() {
return username != null ? username : email;
}
@Override
public String getPassword() {
return password;
} }
@Override @Override
public boolean isAccountNonExpired() { public boolean isAccountNonExpired() {
return isActive; return true;
} }
@Override @Override
public boolean isAccountNonLocked() { public boolean isAccountNonLocked() {
return isActive; return true;
} }
@Override @Override
@ -154,19 +101,14 @@ public class UserEntity implements UserDetails {
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
return enabled && isActive; return enabled;
}
public String getFullName() {
return (firstName != null ? firstName : "") +
(lastName != null ? " " + lastName : "").trim();
} }
@Override @Override
public String toString() { public String toString() {
return "UserEntity(" + super.toString() + String.format( return "UserEntity(" + super.toString() + String.format(
"id=%s, username=%s, firstName=%s, lastName=%s, email=%s, role=%s)", "username=%s, firstName=%s, lastName=%s, email=%s, role=%s)",
id, username, firstName, lastName, email, role.name() username, firstName, lastName, email, role.name()
); );
} }
} }

View File

@ -0,0 +1,18 @@
package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.Address;
import com.dh7789dev.xpeditis.entity.AddressEntity;
import org.mapstruct.Mapper;
import org.mapstruct.MappingConstants;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { CompanyMapper.class })
public interface AddressMapper {
AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
AddressEntity addressToAddressEntity(Address address);
Address addressEntityToAddress(AddressEntity addressEntity);
}

View File

@ -1,39 +1,25 @@
package com.dh7789dev.xpeditis.mapper; package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.Company; import com.dh7789dev.xpeditis.dto.app.Company;
import com.dh7789dev.xpeditis.entity.CompanyEntity; import com.dh7789dev.xpeditis.entity.CompanyEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants;
import org.mapstruct.factory.Mappers;
public class CompanyMapper {
public static CompanyEntity companyToCompanyEntity(Company company) {
if (company == null) return null;
CompanyEntity entity = new CompanyEntity(); @Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
entity.setId(company.getId()); public interface CompanyMapper {
entity.setName(company.getName());
entity.setCountry(company.getCountry());
entity.setSiren(company.getSiren());
entity.setNumEori(company.getNumEori());
entity.setPhone(company.getPhone());
entity.setActive(company.isActive());
entity.setCreatedAt(company.getCreatedAt());
entity.setUpdatedAt(company.getUpdatedAt());
return entity;
}
public static Company companyEntityToCompany(CompanyEntity companyEntity) { CompanyMapper INSTANCE = Mappers.getMapper(CompanyMapper.class);
if (companyEntity == null) return null;
return Company.builder() @Mapping(target = "createdDate", ignore = true)
.id(companyEntity.getId()) @Mapping(target = "modifiedDate", ignore = true)
.name(companyEntity.getName()) @Mapping(target = "createdBy", ignore = true)
.country(companyEntity.getCountry()) @Mapping(target = "modifiedBy", ignore = true)
.siren(companyEntity.getSiren()) CompanyEntity companyToCompanyEntity(Company company);
.numEori(companyEntity.getNumEori())
.phone(companyEntity.getPhone()) Company companyEntityToCompany(CompanyEntity companyEntity);
.isActive(companyEntity.isActive())
.createdAt(companyEntity.getCreatedAt())
.updatedAt(companyEntity.getUpdatedAt())
.build();
}
} }

View File

@ -0,0 +1,17 @@
package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.Dimension;
import com.dh7789dev.xpeditis.entity.DimensionEntity;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface DimensionMapper {
DimensionMapper INSTANCE = Mappers.getMapper(DimensionMapper.class);
DimensionEntity dimensionToDimensionEntity(Dimension dimension);
Dimension dimensionEntityToDimension(DimensionEntity dimensionEntity);
}

View File

@ -0,0 +1,16 @@
package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.Document;
import com.dh7789dev.xpeditis.entity.DocumentEntity;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface DocumentMapper {
DocumentMapper INSTANCE = Mappers.getMapper(DocumentMapper.class);
DocumentEntity documentToDocumentEntity(Document document);
Document documentEntityToDocument(DocumentEntity documentEntity);
}

View File

@ -0,0 +1,21 @@
package com.dh7789dev.xpeditis.mapper;
import com.dh7789dev.xpeditis.dto.app.ExportFolder;
import com.dh7789dev.xpeditis.entity.ExportFolderEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = { DocumentMapper.class, CompanyMapper.class })
public interface ExportFolderMapper {
ExportFolderMapper INSTANCE = Mappers.getMapper(ExportFolderMapper.class);
@Mapping(target = "createdDate", ignore = true)
@Mapping(target = "modifiedDate", ignore = true)
@Mapping(target = "createdBy", ignore = true)
@Mapping(target = "modifiedBy", ignore = true)
ExportFolderEntity exportFolderToCompanyEntity(ExportFolder exportFolder);
ExportFolder exportFolderEntityToExportFolder(ExportFolderEntity exportFolderEntity);
}

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