Compare commits
4 Commits
folder_exp
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0f0de67ca | ||
|
|
428e656598 | ||
|
|
b473c4cbe5 | ||
|
|
1e0b50bd86 |
166
.env.example
166
.env.example
@ -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
105
.gitea/workflows/ci.yml
Executable 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
94
.gitea/workflows/dev.yml
Normal 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
300
CLAUDE.md
@ -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
|
|
||||||
155
ENV_SETUP.md
155
ENV_SETUP.md
@ -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
7
README.md
Normal 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>
|
||||||
@ -1,983 +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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "📦 SSC Export Folders",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "🔍 Rechercher Dossiers",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/export-folders?page=0&size=20&sortBy=dateCreation&sortDir=DESC&statut=CREE&numeroDossier=EXP-2024",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "export-folders"],
|
|
||||||
"query": [
|
|
||||||
{
|
|
||||||
"key": "page",
|
|
||||||
"value": "0",
|
|
||||||
"description": "Numéro de page (0-indexed)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "size",
|
|
||||||
"value": "20",
|
|
||||||
"description": "Taille de page"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "sortBy",
|
|
||||||
"value": "dateCreation",
|
|
||||||
"description": "Champ de tri"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "sortDir",
|
|
||||||
"value": "DESC",
|
|
||||||
"description": "Direction de tri"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "statut",
|
|
||||||
"value": "CREE",
|
|
||||||
"description": "Filtrer par statut",
|
|
||||||
"disabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "numeroDossier",
|
|
||||||
"value": "EXP-2024",
|
|
||||||
"description": "Recherche par numéro de dossier",
|
|
||||||
"disabled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "companyId",
|
|
||||||
"value": "1",
|
|
||||||
"description": "Filtrer par entreprise (admin uniquement)",
|
|
||||||
"disabled": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": [],
|
|
||||||
"protocolProfileBehavior": {
|
|
||||||
"disableBodyPruning": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "📋 Obtenir Dossier par ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/export-folders/1",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "export-folders", "1"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "➕ Créer Dossier depuis Devis",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"quoteId\": 1,\n \"commentairesClient\": \"Demande de transport urgent pour livraison avant fin mars. Marchandises fragiles - manipulation soignée requise.\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/export-folders",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "export-folders"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "🔄 Mettre à jour Statut",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"newStatus\": \"DOCUMENTS_EN_ATTENTE\",\n \"comment\": \"Dossier validé, en attente des documents clients\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/export-folders/1/status",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "export-folders", "1", "status"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "📎 Uploader Document",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "formdata",
|
|
||||||
"formdata": [
|
|
||||||
{
|
|
||||||
"key": "file",
|
|
||||||
"type": "file",
|
|
||||||
"src": "/path/to/your/document.pdf"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "typeDocumentId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "text",
|
|
||||||
"description": "ID du type de document (1=Facture commerciale, 2=Liste de colisage, etc.)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "description",
|
|
||||||
"value": "Facture commerciale pour export maritime",
|
|
||||||
"type": "text",
|
|
||||||
"description": "Description optionnelle"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/export-folders/1/documents",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "export-folders", "1", "documents"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "📑 Lister Documents",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/export-folders/1/documents",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "export-folders", "1", "documents"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "📜 Historique du Dossier",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/export-folders/1/history?limit=50",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "export-folders", "1", "history"],
|
|
||||||
"query": [
|
|
||||||
{
|
|
||||||
"key": "limit",
|
|
||||||
"value": "50",
|
|
||||||
"description": "Nombre maximum d'entrées à retourner"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "🔐 Actions Autorisées",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/export-folders/1/permissions",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "export-folders", "1", "permissions"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "🚢 Système SSC de Gestion des Dossiers d'Export - Workflow complet de création, suivi et gestion documentaire pour les expéditions maritimes",
|
|
||||||
"auth": {
|
|
||||||
"type": "bearer",
|
|
||||||
"bearer": [
|
|
||||||
{
|
|
||||||
"key": "token",
|
|
||||||
"value": "{{jwt_token}}",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "📊 Grilles Tarifaires",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "📋 Lister Grilles Tarifaires",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/grilles-tarifaires?page=0&size=20",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "grilles-tarifaires"],
|
|
||||||
"query": [
|
|
||||||
{
|
|
||||||
"key": "page",
|
|
||||||
"value": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "size",
|
|
||||||
"value": "20"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "nom",
|
|
||||||
"value": "LESCHACO",
|
|
||||||
"disabled": true,
|
|
||||||
"description": "Filtrer par nom"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "paysOrigine",
|
|
||||||
"value": "France",
|
|
||||||
"disabled": true,
|
|
||||||
"description": "Filtrer par pays d'origine"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "🔍 Obtenir Grille par ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/grilles-tarifaires/1",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "grilles-tarifaires", "1"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "✅ Valider Grille Tarifaire",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "formdata",
|
|
||||||
"formdata": [
|
|
||||||
{
|
|
||||||
"key": "file",
|
|
||||||
"type": "file",
|
|
||||||
"src": "/path/to/your/grille.xlsx",
|
|
||||||
"description": "Fichier Excel ou CSV contenant la grille tarifaire"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/grilles-tarifaires/validate",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "grilles-tarifaires", "validate"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "📤 Import CSV",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "formdata",
|
|
||||||
"formdata": [
|
|
||||||
{
|
|
||||||
"key": "file",
|
|
||||||
"type": "file",
|
|
||||||
"src": "/path/to/your/grille.csv"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/grilles-tarifaires/import/csv",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "grilles-tarifaires", "import", "csv"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "📤 Import Excel",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "formdata",
|
|
||||||
"formdata": [
|
|
||||||
{
|
|
||||||
"key": "file",
|
|
||||||
"type": "file",
|
|
||||||
"src": "/path/to/your/grille.xlsx"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/grilles-tarifaires/import/excel",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "grilles-tarifaires", "import", "excel"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "📤 Import JSON",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Authorization",
|
|
||||||
"value": "Bearer {{jwt_token}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"nom\": \"Grille LESCHACO 2025\",\n \"paysOrigine\": \"France\",\n \"paysDestination\": \"Chine\",\n \"portOrigine\": \"FRBOL\",\n \"portDestination\": \"CNSHA\",\n \"dateValiditeDebut\": \"2025-01-01\",\n \"dateValiditeFin\": \"2025-12-31\",\n \"tarifsFret\": [\n {\n \"poidsMin\": 0.0,\n \"poidsMax\": 100.0,\n \"prixParKg\": 2.50,\n \"prixForfaitaire\": 150.00\n },\n {\n \"poidsMin\": 100.0,\n \"poidsMax\": 500.0,\n \"prixParKg\": 2.20,\n \"prixForfaitaire\": 200.00\n }\n ],\n \"fraisAdditionnels\": [\n {\n \"type\": \"MANUTENTION\",\n \"montant\": 45.00,\n \"description\": \"Frais de manutention portuaire\"\n },\n {\n \"type\": \"DOCUMENTATION\",\n \"montant\": 25.00,\n \"description\": \"Frais de documentation\"\n }\n ],\n \"surchargesDangereuses\": [\n {\n \"classe\": \"3\",\n \"pourcentage\": 15.0,\n \"montantFixe\": 100.0,\n \"description\": \"Surcharge marchandises inflammables\"\n }\n ]\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{base_url}}/api/v1/grilles-tarifaires/import/json",
|
|
||||||
"host": ["{{base_url}}"],
|
|
||||||
"path": ["api", "v1", "grilles-tarifaires", "import", "json"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"response": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "🏷️ Gestion des grilles tarifaires LESCHACO - Import, validation et consultation des tarifs pour le calcul automatisé des devis"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"event": [
|
|
||||||
{
|
|
||||||
"listen": "prerequest",
|
|
||||||
"script": {
|
|
||||||
"type": "text/javascript",
|
|
||||||
"exec": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"listen": "test",
|
|
||||||
"script": {
|
|
||||||
"type": "text/javascript",
|
|
||||||
"exec": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ 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;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(value = "/",
|
@RequestMapping(name = "/",
|
||||||
produces = APPLICATION_JSON_VALUE)
|
produces = APPLICATION_JSON_VALUE)
|
||||||
public class IndexRestController {
|
public class IndexRestController {
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,363 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.controller.api.v1;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1/documents")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
@Tag(name = "Documents", description = "Gestion des documents SSC avec validation")
|
|
||||||
@SecurityRequirement(name = "bearerAuth")
|
|
||||||
public class DocumentRestController {
|
|
||||||
|
|
||||||
// TODO: Inject required services
|
|
||||||
// private final DocumentValidationService documentValidationService;
|
|
||||||
// private final DocumentStorageService documentStorageService;
|
|
||||||
// private final ExportFolderPermissionService permissionService;
|
|
||||||
// private final NotificationService notificationService;
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Télécharger un document",
|
|
||||||
description = "Télécharge le fichier document si autorisé",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "Fichier téléchargé"),
|
|
||||||
@ApiResponse(responseCode = "403", description = "Accès refusé"),
|
|
||||||
@ApiResponse(responseCode = "404", description = "Document non trouvé")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@GetMapping("/{id}/download")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<Resource> downloadDocument(
|
|
||||||
@Parameter(description = "ID du document")
|
|
||||||
@PathVariable Long id
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// TODO: Implement download with permission check
|
|
||||||
// UserEntity currentUser = getCurrentUser();
|
|
||||||
// DocumentDossierEntity document = documentService.findById(id);
|
|
||||||
//
|
|
||||||
// if (!permissionService.canPerformAction(currentUser, document.getDossier(), FolderAction.DOWNLOAD_DOCUMENTS)) {
|
|
||||||
// return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Resource resource = documentStorageService.loadAsResource(document);
|
|
||||||
|
|
||||||
log.info("Téléchargement document ID: {}", id);
|
|
||||||
|
|
||||||
// Placeholder response - would return actual file resource
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"document.pdf\"")
|
|
||||||
.body(null); // Would return actual Resource
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors du téléchargement du document {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Valider un document",
|
|
||||||
description = "Approuve ou refuse un document (admin SSC uniquement)",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "Document validé"),
|
|
||||||
@ApiResponse(responseCode = "403", description = "Droits insuffisants"),
|
|
||||||
@ApiResponse(responseCode = "404", description = "Document non trouvé")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@PutMapping("/{id}/validate")
|
|
||||||
@PreAuthorize("hasRole('ADMIN_SSC') or hasRole('SUPER_ADMIN')")
|
|
||||||
public ResponseEntity<DocumentSummaryDto> validateDocument(
|
|
||||||
@Parameter(description = "ID du document")
|
|
||||||
@PathVariable Long id,
|
|
||||||
|
|
||||||
@Parameter(description = "Décision de validation")
|
|
||||||
@Valid @RequestBody ValidationRequest request
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// TODO: Implement document validation
|
|
||||||
// UserEntity currentUser = getCurrentUser();
|
|
||||||
// DocumentSummaryDto validatedDocument = documentValidationService.processValidation(
|
|
||||||
// currentUser, id, request);
|
|
||||||
|
|
||||||
log.info("Validation document {} - décision: {}, commentaire: {}",
|
|
||||||
id, request.isApproved(), request.getComment());
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.ok(DocumentSummaryDto.builder()
|
|
||||||
.id(id)
|
|
||||||
.statutVerification(request.isApproved() ?
|
|
||||||
StatutVerification.VALIDE : StatutVerification.REFUSE)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la validation du document {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Supprimer un document",
|
|
||||||
description = "Supprime un document si autorisé",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(responseCode = "204", description = "Document supprimé"),
|
|
||||||
@ApiResponse(responseCode = "403", description = "Accès refusé"),
|
|
||||||
@ApiResponse(responseCode = "404", description = "Document non trouvé")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<Void> deleteDocument(
|
|
||||||
@Parameter(description = "ID du document")
|
|
||||||
@PathVariable Long id
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// TODO: Implement deletion with permission check
|
|
||||||
// UserEntity currentUser = getCurrentUser();
|
|
||||||
// DocumentDossierEntity document = documentService.findById(id);
|
|
||||||
//
|
|
||||||
// if (!permissionService.canPerformAction(currentUser, document.getDossier(), FolderAction.DELETE_DOCUMENTS)) {
|
|
||||||
// return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// documentValidationService.deleteDocument(currentUser, id);
|
|
||||||
|
|
||||||
log.info("Suppression document ID: {}", id);
|
|
||||||
|
|
||||||
return ResponseEntity.noContent().build();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la suppression du document {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Uploader une nouvelle version",
|
|
||||||
description = "Remplace un document par une nouvelle version",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(responseCode = "201", description = "Nouvelle version uploadée"),
|
|
||||||
@ApiResponse(responseCode = "400", description = "Fichier invalide"),
|
|
||||||
@ApiResponse(responseCode = "403", description = "Accès refusé")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@PostMapping("/{id}/new-version")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<DocumentSummaryDto> uploadNewVersion(
|
|
||||||
@Parameter(description = "ID du document original")
|
|
||||||
@PathVariable Long id,
|
|
||||||
|
|
||||||
@Parameter(description = "Nouveau fichier")
|
|
||||||
@RequestPart("file") MultipartFile file,
|
|
||||||
|
|
||||||
@Parameter(description = "Description des changements")
|
|
||||||
@RequestPart(value = "description", required = false) String description
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// TODO: Implement version upload
|
|
||||||
// UserEntity currentUser = getCurrentUser();
|
|
||||||
// DocumentDossierEntity originalDocument = documentService.findById(id);
|
|
||||||
//
|
|
||||||
// if (!permissionService.canPerformAction(currentUser, originalDocument.getDossier(), FolderAction.UPLOAD_DOCUMENTS)) {
|
|
||||||
// return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// DocumentSummaryDto newVersion = documentValidationService.uploadNewVersion(
|
|
||||||
// currentUser, id, file, description);
|
|
||||||
|
|
||||||
log.info("Upload nouvelle version document {} - fichier: {}", id, file.getOriginalFilename());
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.status(HttpStatus.CREATED)
|
|
||||||
.body(DocumentSummaryDto.builder()
|
|
||||||
.id(id + 1000L) // Simulated new version ID
|
|
||||||
.nomOriginal(file.getOriginalFilename())
|
|
||||||
.numeroVersion(2)
|
|
||||||
.statutVerification(StatutVerification.EN_ATTENTE)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de l'upload de nouvelle version pour document {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Obtenir les détails d'un document",
|
|
||||||
description = "Récupère les informations complètes d'un document",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "Détails du document"),
|
|
||||||
@ApiResponse(responseCode = "403", description = "Accès refusé"),
|
|
||||||
@ApiResponse(responseCode = "404", description = "Document non trouvé")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<DocumentDetailDto> getDocumentDetails(
|
|
||||||
@Parameter(description = "ID du document")
|
|
||||||
@PathVariable Long id
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// TODO: Implement details retrieval with permissions
|
|
||||||
// UserEntity currentUser = getCurrentUser();
|
|
||||||
// DocumentDetailDto details = documentService.getDetailsWithPermissions(currentUser, id);
|
|
||||||
|
|
||||||
log.info("Consultation détails document ID: {}", id);
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.ok(DocumentDetailDto.builder()
|
|
||||||
.id(id)
|
|
||||||
.statutVerification(StatutVerification.EN_ATTENTE)
|
|
||||||
.build());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la récupération des détails du document {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Marquer un document comme expiré",
|
|
||||||
description = "Force l'expiration d'un document (admin uniquement)",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "Document marqué expiré"),
|
|
||||||
@ApiResponse(responseCode = "403", description = "Droits insuffisants"),
|
|
||||||
@ApiResponse(responseCode = "404", description = "Document non trouvé")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@PutMapping("/{id}/expire")
|
|
||||||
@PreAuthorize("hasRole('ADMIN_SSC') or hasRole('SUPER_ADMIN')")
|
|
||||||
public ResponseEntity<Void> expireDocument(
|
|
||||||
@Parameter(description = "ID du document")
|
|
||||||
@PathVariable Long id,
|
|
||||||
|
|
||||||
@Parameter(description = "Raison de l'expiration")
|
|
||||||
@RequestBody ExpireDocumentRequest request
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// TODO: Implement document expiration
|
|
||||||
// UserEntity currentUser = getCurrentUser();
|
|
||||||
// documentValidationService.expireDocument(currentUser, id, request.getReason());
|
|
||||||
|
|
||||||
log.info("Expiration document {} - raison: {}", id, request.getReason());
|
|
||||||
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de l'expiration du document {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Obtenir l'historique de validation d'un document",
|
|
||||||
description = "Liste chronologique des validations d'un document",
|
|
||||||
responses = {
|
|
||||||
@ApiResponse(responseCode = "200", description = "Historique de validation"),
|
|
||||||
@ApiResponse(responseCode = "403", description = "Accès refusé"),
|
|
||||||
@ApiResponse(responseCode = "404", description = "Document non trouvé")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@GetMapping("/{id}/validation-history")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<java.util.List<ValidationHistoryDto>> getValidationHistory(
|
|
||||||
@Parameter(description = "ID du document")
|
|
||||||
@PathVariable Long id
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// TODO: Implement validation history retrieval
|
|
||||||
// UserEntity currentUser = getCurrentUser();
|
|
||||||
// List<ValidationHistoryDto> history = documentService.getValidationHistory(currentUser, id);
|
|
||||||
|
|
||||||
log.info("Consultation historique validation document ID: {}", id);
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.ok(java.util.List.of());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la récupération de l'historique de validation du document {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add getCurrentUser() method to get authenticated user
|
|
||||||
// private UserEntity getCurrentUser() {
|
|
||||||
// // Implementation to get current authenticated user
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== REQUEST DTOs ==========
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
class ValidationRequest {
|
|
||||||
private boolean approved;
|
|
||||||
private String comment;
|
|
||||||
private String correctionsDemandees;
|
|
||||||
}
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
class ExpireDocumentRequest {
|
|
||||||
private String reason;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== RESPONSE DTOs ==========
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
@lombok.Builder
|
|
||||||
@lombok.NoArgsConstructor
|
|
||||||
@lombok.AllArgsConstructor
|
|
||||||
class DocumentDetailDto {
|
|
||||||
private Long id;
|
|
||||||
private String nomOriginal;
|
|
||||||
private String typeDocumentNom;
|
|
||||||
private Long tailleOctets;
|
|
||||||
private String typeMime;
|
|
||||||
private Integer numeroVersion;
|
|
||||||
private StatutVerification statutVerification;
|
|
||||||
private String commentaireVerification;
|
|
||||||
private String correctionsDemandees;
|
|
||||||
private String description;
|
|
||||||
private java.time.LocalDate dateValidite;
|
|
||||||
private boolean isExpired;
|
|
||||||
private String uploadePar;
|
|
||||||
private java.time.LocalDateTime dateUpload;
|
|
||||||
private String verifiePar;
|
|
||||||
private java.time.LocalDateTime dateVerification;
|
|
||||||
private boolean canDownload;
|
|
||||||
private boolean canDelete;
|
|
||||||
private boolean canValidate;
|
|
||||||
private boolean canUploadNewVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
@lombok.Builder
|
|
||||||
@lombok.NoArgsConstructor
|
|
||||||
@lombok.AllArgsConstructor
|
|
||||||
class ValidationHistoryDto {
|
|
||||||
private Long id;
|
|
||||||
private StatutVerification ancienStatut;
|
|
||||||
private StatutVerification nouveauStatut;
|
|
||||||
private String commentaire;
|
|
||||||
private String effectuePar;
|
|
||||||
private java.time.LocalDateTime dateValidation;
|
|
||||||
private String reason;
|
|
||||||
}
|
|
||||||
@ -1,274 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.controller.api.v1;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.*;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
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.data.domain.Sort;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1/export-folders")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
@Slf4j
|
|
||||||
@Tag(name = "Export Folders", description = "Gestion des dossiers d'export SSC")
|
|
||||||
@SecurityRequirement(name = "bearerAuth")
|
|
||||||
public class ExportFolderRestController {
|
|
||||||
|
|
||||||
// TODO: Inject permission service when modules are properly connected
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Rechercher des dossiers d'export",
|
|
||||||
description = "Recherche paginée avec filtres selon permissions utilisateur"
|
|
||||||
)
|
|
||||||
@GetMapping
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<Page<ExportFolderDto>> searchFolders(
|
|
||||||
@Parameter(description = "Critères de recherche")
|
|
||||||
@RequestParam Map<String, String> params,
|
|
||||||
|
|
||||||
@Parameter(description = "Numéro de page (0-indexed)")
|
|
||||||
@RequestParam(defaultValue = "0") int page,
|
|
||||||
|
|
||||||
@Parameter(description = "Taille de page")
|
|
||||||
@RequestParam(defaultValue = "20") int size,
|
|
||||||
|
|
||||||
@Parameter(description = "Champ de tri")
|
|
||||||
@RequestParam(defaultValue = "dateCreation") String sortBy,
|
|
||||||
|
|
||||||
@Parameter(description = "Direction de tri")
|
|
||||||
@RequestParam(defaultValue = "DESC") String sortDir
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
Pageable pageable = PageRequest.of(
|
|
||||||
page, size,
|
|
||||||
Sort.Direction.fromString(sortDir),
|
|
||||||
sortBy
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info("Recherche dossiers - page: {}, size: {}, filtres: {}", page, size, params);
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.ok(Page.empty());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la recherche de dossiers", e);
|
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Créer un dossier d'export depuis un devis",
|
|
||||||
description = "Crée un nouveau dossier d'export à partir d'un devis accepté"
|
|
||||||
)
|
|
||||||
@PostMapping
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<ExportFolderDto> createFromQuote(
|
|
||||||
@Parameter(description = "Données pour création du dossier")
|
|
||||||
@Valid @RequestBody CreateFolderRequest request
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
log.info("Création dossier depuis devis ID: {}", request.getQuoteId());
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.status(HttpStatus.CREATED)
|
|
||||||
.body(ExportFolderDto.builder().build());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la création du dossier", e);
|
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Obtenir un dossier par ID",
|
|
||||||
description = "Récupère les détails complets d'un dossier selon permissions"
|
|
||||||
)
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<ExportFolderDto> getFolder(
|
|
||||||
@Parameter(description = "ID du dossier")
|
|
||||||
@PathVariable Long id
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
log.info("Consultation dossier ID: {}", id);
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.ok(ExportFolderDto.builder().id(id).build());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la récupération du dossier {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Mettre à jour le statut d'un dossier",
|
|
||||||
description = "Change le statut d'un dossier selon le workflow"
|
|
||||||
)
|
|
||||||
@PutMapping("/{id}/status")
|
|
||||||
@PreAuthorize("hasRole('ADMIN_SSC') or hasRole('SUPER_ADMIN')")
|
|
||||||
public ResponseEntity<Void> updateStatus(
|
|
||||||
@Parameter(description = "ID du dossier")
|
|
||||||
@PathVariable Long id,
|
|
||||||
|
|
||||||
@Parameter(description = "Nouveau statut")
|
|
||||||
@Valid @RequestBody StatusUpdateRequest request
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
log.info("Mise à jour statut dossier {} vers {}", id, request.getNewStatus());
|
|
||||||
|
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la mise à jour du statut du dossier {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Uploader un document",
|
|
||||||
description = "Ajoute un nouveau document au dossier"
|
|
||||||
)
|
|
||||||
@PostMapping("/{id}/documents")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<DocumentSummaryDto> uploadDocument(
|
|
||||||
@Parameter(description = "ID du dossier")
|
|
||||||
@PathVariable Long id,
|
|
||||||
|
|
||||||
@Parameter(description = "Fichier à uploader")
|
|
||||||
@RequestPart("file") MultipartFile file,
|
|
||||||
|
|
||||||
@Parameter(description = "ID du type de document")
|
|
||||||
@RequestPart("typeDocumentId") Long typeDocumentId,
|
|
||||||
|
|
||||||
@Parameter(description = "Description optionnelle")
|
|
||||||
@RequestPart(value = "description", required = false) String description
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
log.info("Upload document pour dossier {} - fichier: {}, type: {}",
|
|
||||||
id, file.getOriginalFilename(), typeDocumentId);
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.status(HttpStatus.CREATED)
|
|
||||||
.body(DocumentSummaryDto.builder().build());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de l'upload de document pour dossier {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Obtenir les documents d'un dossier",
|
|
||||||
description = "Liste tous les documents avec statuts de validation"
|
|
||||||
)
|
|
||||||
@GetMapping("/{id}/documents")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<List<DocumentSummaryDto>> getDocuments(
|
|
||||||
@Parameter(description = "ID du dossier")
|
|
||||||
@PathVariable Long id
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
log.info("Consultation documents dossier ID: {}", id);
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.ok(List.of());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la récupération des documents du dossier {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Obtenir l'historique d'un dossier",
|
|
||||||
description = "Liste chronologique des actions sur le dossier"
|
|
||||||
)
|
|
||||||
@GetMapping("/{id}/history")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<List<HistoryEntryDto>> getHistory(
|
|
||||||
@Parameter(description = "ID du dossier")
|
|
||||||
@PathVariable Long id,
|
|
||||||
|
|
||||||
@Parameter(description = "Nombre d'entrées max")
|
|
||||||
@RequestParam(defaultValue = "50") int limit
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
log.info("Consultation historique dossier ID: {}, limit: {}", id, limit);
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.ok(List.of());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la récupération de l'historique du dossier {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Obtenir les actions autorisées",
|
|
||||||
description = "Liste les actions que l'utilisateur peut effectuer sur ce dossier"
|
|
||||||
)
|
|
||||||
@GetMapping("/{id}/permissions")
|
|
||||||
@PreAuthorize("hasRole('COMPANY_USER') or hasRole('ADMIN')")
|
|
||||||
public ResponseEntity<Set<FolderAction>> getAuthorizedActions(
|
|
||||||
@Parameter(description = "ID du dossier")
|
|
||||||
@PathVariable Long id
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
log.info("Consultation permissions dossier ID: {}", id);
|
|
||||||
|
|
||||||
// Placeholder response
|
|
||||||
return ResponseEntity.ok(Set.of(FolderAction.VIEW_BASIC_INFO));
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Erreur lors de la récupération des permissions du dossier {}", id, e);
|
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== REQUEST DTOs ==========
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
class CreateFolderRequest {
|
|
||||||
private Long quoteId;
|
|
||||||
private String commentairesClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
class StatusUpdateRequest {
|
|
||||||
private DossierStatus newStatus;
|
|
||||||
private String comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
class AssignFolderRequest {
|
|
||||||
private Long adminId;
|
|
||||||
private String comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
@lombok.Data
|
|
||||||
class UpdateTransportRequest {
|
|
||||||
private String referenceBooking;
|
|
||||||
private String numeroBl;
|
|
||||||
private String numeroConteneur;
|
|
||||||
private String notesInternes;
|
|
||||||
}
|
|
||||||
@ -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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,171 +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.List;
|
|
||||||
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);
|
|
||||||
|
|
||||||
List<UserAccount> userAccounts;
|
|
||||||
long totalElements;
|
|
||||||
|
|
||||||
if (companyId != null) {
|
|
||||||
userAccounts = userService.findUsersByCompany(companyId, page, size);
|
|
||||||
totalElements = userService.countUsersByCompany(companyId);
|
|
||||||
} else {
|
|
||||||
userAccounts = userService.findAllUsers(page, size);
|
|
||||||
totalElements = userService.countAllUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to UserResponse DTOs
|
|
||||||
List<UserResponse> userResponses = userAccounts.stream()
|
|
||||||
.map(this::mapToUserResponse)
|
|
||||||
.collect(java.util.stream.Collectors.toList());
|
|
||||||
|
|
||||||
// Create Page object
|
|
||||||
Pageable pageable = PageRequest.of(page, size);
|
|
||||||
Page<UserResponse> users = new org.springframework.data.domain.PageImpl<>(
|
|
||||||
userResponses,
|
|
||||||
pageable,
|
|
||||||
totalElements);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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";
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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}
|
|
||||||
@ -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}
|
|
||||||
@ -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:
|
||||||
|
|||||||
14
bootstrap/src/test/java/com/dh7789dev/xpeditis/LeBlrApplicationTests.java
Executable file
14
bootstrap/src/test/java/com/dh7789dev/xpeditis/LeBlrApplicationTests.java
Executable 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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,44 +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.List;
|
|
||||||
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);
|
|
||||||
|
|
||||||
List<UserAccount> findAllUsers(int page, int size);
|
|
||||||
|
|
||||||
List<UserAccount> findUsersByCompany(UUID companyId, int page, int size);
|
|
||||||
|
|
||||||
long countAllUsers();
|
|
||||||
|
|
||||||
long countUsersByCompany(UUID companyId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
|
||||||
|
|
||||||
public enum AuthProvider {
|
|
||||||
LOCAL,
|
|
||||||
GOOGLE
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class DocumentSummaryDto {
|
|
||||||
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
// ========== DOCUMENT TYPE ==========
|
|
||||||
private Long typeDocumentId;
|
|
||||||
private String typeDocumentNom;
|
|
||||||
private boolean isObligatoire;
|
|
||||||
|
|
||||||
// ========== FILE INFO ==========
|
|
||||||
private String nomOriginal;
|
|
||||||
private Long tailleOctets;
|
|
||||||
private String typeMime;
|
|
||||||
private Integer numeroVersion;
|
|
||||||
|
|
||||||
// ========== VALIDATION ==========
|
|
||||||
private StatutVerification statutVerification;
|
|
||||||
private String displayStatutVerification;
|
|
||||||
private String commentaireVerification;
|
|
||||||
private String correctionsDemandees;
|
|
||||||
|
|
||||||
// ========== METADATA ==========
|
|
||||||
private String description;
|
|
||||||
private LocalDate dateValidite;
|
|
||||||
private boolean isExpired;
|
|
||||||
|
|
||||||
// ========== USER INFO ==========
|
|
||||||
private String uploadePar;
|
|
||||||
private LocalDateTime dateUpload;
|
|
||||||
private String verifiePar;
|
|
||||||
private LocalDateTime dateVerification;
|
|
||||||
|
|
||||||
// ========== ACTIONS ==========
|
|
||||||
private boolean canDownload;
|
|
||||||
private boolean canDelete;
|
|
||||||
private boolean canValidate;
|
|
||||||
private boolean requiresAdminAction;
|
|
||||||
}
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Énumération des statuts d'un dossier d'export
|
|
||||||
* Définit le workflow complet de traitement des dossiers
|
|
||||||
*/
|
|
||||||
public enum DossierStatus {
|
|
||||||
// ========== PHASE CRÉATION ==========
|
|
||||||
CREE("Créé", "Dossier créé depuis devis accepté", false, false),
|
|
||||||
|
|
||||||
// ========== PHASE DOCUMENTS ==========
|
|
||||||
DOCUMENTS_EN_ATTENTE("Documents en attente", "En attente d'upload des documents obligatoires", true, false),
|
|
||||||
DOCUMENTS_UPLOADES("Documents uploadés", "Documents uploadés, en attente de vérification", true, false),
|
|
||||||
DOCUMENTS_EN_VERIFICATION("Documents en vérification", "Vérification des documents en cours par admin", false, false),
|
|
||||||
DOCUMENTS_REFUSES("Documents refusés", "Corrections demandées sur un ou plusieurs documents", true, false),
|
|
||||||
DOCUMENTS_VALIDES("Documents validés", "Tous documents validés, prêt pour booking", false, false),
|
|
||||||
|
|
||||||
// ========== PHASE TRANSPORT ==========
|
|
||||||
BOOKING_EN_COURS("Booking en cours", "Réservation transport en cours", false, false),
|
|
||||||
BOOKING_CONFIRME("Booking confirmé", "Transport confirmé", false, false),
|
|
||||||
ENLEVE("Enlevé", "Marchandise enlevée", false, false),
|
|
||||||
EN_TRANSIT("En transit", "En cours de transport", false, false),
|
|
||||||
ARRIVE("Arrivé", "Arrivé à destination", false, false),
|
|
||||||
|
|
||||||
// ========== PHASE FINALISATION ==========
|
|
||||||
LIVRE("Livré", "Livré au destinataire", false, true),
|
|
||||||
CLOTURE("Clôturé", "Dossier clôturé", false, true),
|
|
||||||
|
|
||||||
// ========== STATUT SPÉCIAL ==========
|
|
||||||
ANNULE("Annulé", "Dossier annulé", false, true);
|
|
||||||
|
|
||||||
private final String displayName;
|
|
||||||
private final String description;
|
|
||||||
private final boolean allowsDocumentUpload;
|
|
||||||
private final boolean isFinalized;
|
|
||||||
|
|
||||||
DossierStatus(String displayName, String description, boolean allowsDocumentUpload, boolean isFinalized) {
|
|
||||||
this.displayName = displayName;
|
|
||||||
this.description = description;
|
|
||||||
this.allowsDocumentUpload = allowsDocumentUpload;
|
|
||||||
this.isFinalized = isFinalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplayName() {
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean allowsDocumentUpload() {
|
|
||||||
return allowsDocumentUpload;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFinalized() {
|
|
||||||
return isFinalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retourne les statuts qui permettent l'upload de documents
|
|
||||||
*/
|
|
||||||
public static Set<DossierStatus> getUploadableStatuses() {
|
|
||||||
return Set.of(
|
|
||||||
DOCUMENTS_EN_ATTENTE,
|
|
||||||
DOCUMENTS_UPLOADES,
|
|
||||||
DOCUMENTS_REFUSES
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retourne les statuts considérés comme actifs (non terminés)
|
|
||||||
*/
|
|
||||||
public static Set<DossierStatus> getActiveStatuses() {
|
|
||||||
return Set.of(
|
|
||||||
CREE,
|
|
||||||
DOCUMENTS_EN_ATTENTE,
|
|
||||||
DOCUMENTS_UPLOADES,
|
|
||||||
DOCUMENTS_EN_VERIFICATION,
|
|
||||||
DOCUMENTS_REFUSES,
|
|
||||||
DOCUMENTS_VALIDES,
|
|
||||||
BOOKING_EN_COURS,
|
|
||||||
BOOKING_CONFIRME,
|
|
||||||
ENLEVE,
|
|
||||||
EN_TRANSIT,
|
|
||||||
ARRIVE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si une transition de statut est valide
|
|
||||||
*/
|
|
||||||
public boolean canTransitionTo(DossierStatus newStatus) {
|
|
||||||
if (this.isFinalized) {
|
|
||||||
return false; // Aucune transition possible depuis un statut finalisé
|
|
||||||
}
|
|
||||||
|
|
||||||
// Définition des transitions valides
|
|
||||||
switch (this) {
|
|
||||||
case CREE:
|
|
||||||
return newStatus == DOCUMENTS_EN_ATTENTE || newStatus == ANNULE;
|
|
||||||
|
|
||||||
case DOCUMENTS_EN_ATTENTE:
|
|
||||||
return newStatus == DOCUMENTS_UPLOADES || newStatus == ANNULE;
|
|
||||||
|
|
||||||
case DOCUMENTS_UPLOADES:
|
|
||||||
return newStatus == DOCUMENTS_EN_VERIFICATION ||
|
|
||||||
newStatus == DOCUMENTS_EN_ATTENTE || newStatus == ANNULE;
|
|
||||||
|
|
||||||
case DOCUMENTS_EN_VERIFICATION:
|
|
||||||
return newStatus == DOCUMENTS_VALIDES ||
|
|
||||||
newStatus == DOCUMENTS_REFUSES || newStatus == ANNULE;
|
|
||||||
|
|
||||||
case DOCUMENTS_REFUSES:
|
|
||||||
return newStatus == DOCUMENTS_UPLOADES ||
|
|
||||||
newStatus == DOCUMENTS_EN_VERIFICATION || newStatus == ANNULE;
|
|
||||||
|
|
||||||
case DOCUMENTS_VALIDES:
|
|
||||||
return newStatus == BOOKING_EN_COURS || newStatus == ANNULE;
|
|
||||||
|
|
||||||
case BOOKING_EN_COURS:
|
|
||||||
return newStatus == BOOKING_CONFIRME ||
|
|
||||||
newStatus == DOCUMENTS_VALIDES || newStatus == ANNULE;
|
|
||||||
|
|
||||||
case BOOKING_CONFIRME:
|
|
||||||
return newStatus == ENLEVE || newStatus == ANNULE;
|
|
||||||
|
|
||||||
case ENLEVE:
|
|
||||||
return newStatus == EN_TRANSIT;
|
|
||||||
|
|
||||||
case EN_TRANSIT:
|
|
||||||
return newStatus == ARRIVE;
|
|
||||||
|
|
||||||
case ARRIVE:
|
|
||||||
return newStatus == LIVRE;
|
|
||||||
|
|
||||||
case LIVRE:
|
|
||||||
return newStatus == CLOTURE;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retourne le prochain statut logique dans le workflow standard
|
|
||||||
*/
|
|
||||||
public DossierStatus getNextLogicalStatus() {
|
|
||||||
switch (this) {
|
|
||||||
case CREE: return DOCUMENTS_EN_ATTENTE;
|
|
||||||
case DOCUMENTS_EN_ATTENTE: return DOCUMENTS_UPLOADES;
|
|
||||||
case DOCUMENTS_UPLOADES: return DOCUMENTS_EN_VERIFICATION;
|
|
||||||
case DOCUMENTS_EN_VERIFICATION: return DOCUMENTS_VALIDES;
|
|
||||||
case DOCUMENTS_REFUSES: return DOCUMENTS_UPLOADES;
|
|
||||||
case DOCUMENTS_VALIDES: return BOOKING_EN_COURS;
|
|
||||||
case BOOKING_EN_COURS: return BOOKING_CONFIRME;
|
|
||||||
case BOOKING_CONFIRME: return ENLEVE;
|
|
||||||
case ENLEVE: return EN_TRANSIT;
|
|
||||||
case EN_TRANSIT: return ARRIVE;
|
|
||||||
case ARRIVE: return LIVRE;
|
|
||||||
case LIVRE: return CLOTURE;
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,79 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class ExportFolderDto {
|
|
||||||
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
// ========== BASIC INFO ==========
|
|
||||||
private String reference;
|
|
||||||
private String numeroDossier;
|
|
||||||
private DossierStatus statut;
|
|
||||||
private String displayStatut;
|
|
||||||
|
|
||||||
// ========== COMPANY & QUOTE ==========
|
|
||||||
private Long companyId;
|
|
||||||
private String companyName;
|
|
||||||
private Long quoteId;
|
|
||||||
private String quoteReference;
|
|
||||||
|
|
||||||
// ========== WORKFLOW DATES ==========
|
|
||||||
private LocalDateTime dateCreation;
|
|
||||||
private LocalDateTime dateDocumentsComplets;
|
|
||||||
private LocalDateTime dateValidationDocuments;
|
|
||||||
private LocalDateTime dateBooking;
|
|
||||||
private LocalDateTime dateEnlevement;
|
|
||||||
private LocalDateTime dateLivraison;
|
|
||||||
private LocalDateTime dateCloture;
|
|
||||||
|
|
||||||
// ========== TRANSPORT REFERENCES ==========
|
|
||||||
private String referenceBooking;
|
|
||||||
private String numeroBl;
|
|
||||||
private String numeroConteneur;
|
|
||||||
|
|
||||||
// ========== METADATA ==========
|
|
||||||
private String commentairesClient;
|
|
||||||
private String notesInternes;
|
|
||||||
|
|
||||||
// ========== USER ASSIGNMENTS ==========
|
|
||||||
private Long createdById;
|
|
||||||
private String createdByName;
|
|
||||||
private Long assignedToAdminId;
|
|
||||||
private String assignedToAdminName;
|
|
||||||
|
|
||||||
// ========== WORKFLOW INFO ==========
|
|
||||||
private boolean allowsDocumentUpload;
|
|
||||||
private boolean isFinalized;
|
|
||||||
private DossierStatus nextLogicalStatus;
|
|
||||||
private boolean canTransitionToNext;
|
|
||||||
|
|
||||||
// ========== DOCUMENTS SUMMARY ==========
|
|
||||||
private int totalDocuments;
|
|
||||||
private int validatedDocuments;
|
|
||||||
private int pendingDocuments;
|
|
||||||
private int rejectedDocuments;
|
|
||||||
private boolean allMandatoryDocumentsProvided;
|
|
||||||
|
|
||||||
// ========== PERMISSIONS ==========
|
|
||||||
private Set<FolderAction> authorizedActions;
|
|
||||||
|
|
||||||
// ========== RELATED DATA ==========
|
|
||||||
private List<DocumentSummaryDto> documents;
|
|
||||||
private List<HistoryEntryDto> recentHistory;
|
|
||||||
|
|
||||||
// ========== TIMESTAMPS ==========
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
private LocalDateTime modifiedAt;
|
|
||||||
}
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions possibles sur les dossiers d'export
|
|
||||||
* Utilisé pour le système de permissions granulaires
|
|
||||||
*/
|
|
||||||
public enum FolderAction {
|
|
||||||
|
|
||||||
// ========== CONSULTATION ==========
|
|
||||||
VIEW_BASIC_INFO("Consulter informations de base", "Voir référence, statut, dates"),
|
|
||||||
VIEW_FULL_DETAILS("Consulter détails complets", "Voir toutes les informations du dossier"),
|
|
||||||
VIEW_DOCUMENTS("Consulter documents", "Voir la liste des documents uploadés"),
|
|
||||||
VIEW_HISTORY("Consulter historique", "Voir l'historique des actions"),
|
|
||||||
DOWNLOAD_DOCUMENTS("Télécharger documents", "Télécharger les fichiers"),
|
|
||||||
|
|
||||||
// ========== MODIFICATION ==========
|
|
||||||
UPDATE_BASIC_INFO("Modifier informations de base", "Modifier commentaires client, références"),
|
|
||||||
UPDATE_STATUS("Modifier statut", "Changer le statut du dossier"),
|
|
||||||
ADD_INTERNAL_NOTES("Ajouter notes internes", "Ajouter des commentaires internes"),
|
|
||||||
|
|
||||||
// ========== GESTION DOCUMENTS ==========
|
|
||||||
UPLOAD_DOCUMENTS("Uploader documents", "Ajouter de nouveaux documents"),
|
|
||||||
DELETE_DOCUMENTS("Supprimer documents", "Supprimer des documents"),
|
|
||||||
VALIDATE_DOCUMENTS("Valider documents", "Approuver ou refuser des documents"),
|
|
||||||
REQUEST_CORRECTIONS("Demander corrections", "Demander des corrections sur documents"),
|
|
||||||
|
|
||||||
// ========== WORKFLOW ==========
|
|
||||||
TRANSITION_STATUS("Changer statut workflow", "Faire progresser le dossier dans le workflow"),
|
|
||||||
FORCE_STATUS_CHANGE("Forcer changement statut", "Changer le statut sans validation workflow"),
|
|
||||||
ASSIGN_TO_ADMIN("Assigner à administrateur", "Assigner le dossier à un admin"),
|
|
||||||
MARK_DOCUMENTS_COMPLETE("Marquer documents complets", "Valider que tous documents sont fournis"),
|
|
||||||
|
|
||||||
// ========== ADMINISTRATION ==========
|
|
||||||
DELETE_FOLDER("Supprimer dossier", "Supprimer complètement le dossier"),
|
|
||||||
EXPORT_DATA("Exporter données", "Exporter les données du dossier"),
|
|
||||||
MANAGE_PERMISSIONS("Gérer permissions", "Modifier les permissions sur le dossier"),
|
|
||||||
ACCESS_SYSTEM_DATA("Accès données système", "Voir données techniques internes"),
|
|
||||||
|
|
||||||
// ========== TRANSPORT ==========
|
|
||||||
UPDATE_TRANSPORT_INFO("Modifier infos transport", "Mettre à jour booking, BL, conteneur"),
|
|
||||||
TRACK_SHIPMENT("Suivre expédition", "Voir le suivi de l'expédition"),
|
|
||||||
UPDATE_TRACKING("Mettre à jour suivi", "Ajouter des mises à jour de suivi"),
|
|
||||||
|
|
||||||
// ========== NOTIFICATIONS ==========
|
|
||||||
RECEIVE_NOTIFICATIONS("Recevoir notifications", "Être notifié des changements"),
|
|
||||||
SEND_NOTIFICATIONS("Envoyer notifications", "Notifier d'autres utilisateurs");
|
|
||||||
|
|
||||||
private final String displayName;
|
|
||||||
private final String description;
|
|
||||||
|
|
||||||
FolderAction(String displayName, String description) {
|
|
||||||
this.displayName = displayName;
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplayName() {
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions de consultation seulement
|
|
||||||
*/
|
|
||||||
public boolean isReadOnlyAction() {
|
|
||||||
return this == VIEW_BASIC_INFO || this == VIEW_FULL_DETAILS ||
|
|
||||||
this == VIEW_DOCUMENTS || this == VIEW_HISTORY ||
|
|
||||||
this == DOWNLOAD_DOCUMENTS || this == TRACK_SHIPMENT ||
|
|
||||||
this == RECEIVE_NOTIFICATIONS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions nécessitant des privilèges administratifs
|
|
||||||
*/
|
|
||||||
public boolean requiresAdminPrivileges() {
|
|
||||||
return this == VALIDATE_DOCUMENTS || this == FORCE_STATUS_CHANGE ||
|
|
||||||
this == DELETE_FOLDER || this == MANAGE_PERMISSIONS ||
|
|
||||||
this == ACCESS_SYSTEM_DATA || this == ASSIGN_TO_ADMIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions liées au workflow
|
|
||||||
*/
|
|
||||||
public boolean isWorkflowAction() {
|
|
||||||
return this == UPDATE_STATUS || this == TRANSITION_STATUS ||
|
|
||||||
this == FORCE_STATUS_CHANGE || this == MARK_DOCUMENTS_COMPLETE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class HistoryEntryDto {
|
|
||||||
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
// ========== ACTION INFO ==========
|
|
||||||
private String action;
|
|
||||||
private String description;
|
|
||||||
|
|
||||||
// ========== STATUS CHANGES ==========
|
|
||||||
private String ancienStatut;
|
|
||||||
private String nouveauStatut;
|
|
||||||
|
|
||||||
// ========== ACTOR INFO ==========
|
|
||||||
private String effectueParType; // COMPANY_USER, ADMIN, SYSTEM
|
|
||||||
private Long effectueParId;
|
|
||||||
private String effectueParNom;
|
|
||||||
|
|
||||||
// ========== TIMING ==========
|
|
||||||
private LocalDateTime dateAction;
|
|
||||||
private String relativeTime; // "Il y a 2 heures", "Hier", etc.
|
|
||||||
|
|
||||||
// ========== ADDITIONAL DATA ==========
|
|
||||||
private Map<String, Object> donneesSupplementaires;
|
|
||||||
|
|
||||||
// ========== UI HELPERS ==========
|
|
||||||
private String actionIcon; // Icon CSS class for UI
|
|
||||||
private String actionColor; // Color for UI display
|
|
||||||
private String displayText; // Human-readable display text
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Système de rôles étendu pour SSC Export System
|
|
||||||
* Intègre les rôles existants et les nouveaux besoins SSC
|
|
||||||
*/
|
|
||||||
public enum Role {
|
|
||||||
// ========== RÔLES EXISTANTS (Conservés) ==========
|
|
||||||
USER("Utilisateur Standard", 1),
|
|
||||||
MANAGER("Gestionnaire", 2),
|
|
||||||
ADMIN("Administrateur", 3),
|
|
||||||
ADMIN_PLATFORM("Administrateur Plateforme", 4),
|
|
||||||
|
|
||||||
// ========== NOUVEAUX RÔLES SSC ==========
|
|
||||||
SUPER_ADMIN("Super Administrateur", 10), // Accès total système
|
|
||||||
ADMIN_SSC("Administrateur SSC", 9), // Validation documents, gestion dossiers
|
|
||||||
COMPANY_ADMIN("Administrateur Entreprise", 6), // Gestion équipe entreprise cliente
|
|
||||||
COMPANY_USER("Utilisateur Entreprise", 5), // Consultation dossiers de son entreprise
|
|
||||||
COMPANY_GUEST("Invité Entreprise", 3); // Lecture seule sur dossiers spécifiques
|
|
||||||
|
|
||||||
private final String displayName;
|
|
||||||
private final int level;
|
|
||||||
|
|
||||||
Role(String displayName, int level) {
|
|
||||||
this.displayName = displayName;
|
|
||||||
this.level = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplayName() {
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLevel() {
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si ce rôle a un niveau supérieur ou égal à un autre
|
|
||||||
*/
|
|
||||||
public boolean hasLevelGreaterOrEqual(Role other) {
|
|
||||||
return this.level >= other.level;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si ce rôle est un rôle SSC (gestion dossiers export)
|
|
||||||
*/
|
|
||||||
public boolean isSscRole() {
|
|
||||||
return this == SUPER_ADMIN || this == ADMIN_SSC ||
|
|
||||||
this == COMPANY_ADMIN || this == COMPANY_USER || this == COMPANY_GUEST;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si ce rôle peut gérer des utilisateurs d'entreprise
|
|
||||||
*/
|
|
||||||
public boolean canManageCompanyUsers() {
|
|
||||||
return this == SUPER_ADMIN || this == ADMIN_SSC || this == COMPANY_ADMIN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si ce rôle peut valider des documents
|
|
||||||
*/
|
|
||||||
public boolean canValidateDocuments() {
|
|
||||||
return this == SUPER_ADMIN || this == ADMIN_SSC;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si ce rôle est un administrateur
|
|
||||||
*/
|
|
||||||
public boolean isAdmin() {
|
|
||||||
return this == SUPER_ADMIN || this == ADMIN_SSC ||
|
|
||||||
this == ADMIN || this == ADMIN_PLATFORM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.app;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Statuts de vérification des documents uploadés
|
|
||||||
* Cycle de validation par les administrateurs SSC
|
|
||||||
*/
|
|
||||||
public enum StatutVerification {
|
|
||||||
|
|
||||||
EN_ATTENTE("En attente", "Document uploadé, en attente de vérification"),
|
|
||||||
EN_COURS_VERIFICATION("En cours de vérification", "Document en cours d'analyse par un administrateur"),
|
|
||||||
VALIDE("Validé", "Document vérifié et approuvé"),
|
|
||||||
REFUSE("Refusé", "Document refusé, corrections nécessaires"),
|
|
||||||
EXPIRE("Expiré", "Document expiré, renouvellement requis");
|
|
||||||
|
|
||||||
private final String displayName;
|
|
||||||
private final String description;
|
|
||||||
|
|
||||||
StatutVerification(String displayName, String description) {
|
|
||||||
this.displayName = displayName;
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplayName() {
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si ce statut permet une nouvelle vérification
|
|
||||||
*/
|
|
||||||
public boolean allowsReVerification() {
|
|
||||||
return this == REFUSE || this == EXPIRE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si ce statut est considéré comme final
|
|
||||||
*/
|
|
||||||
public boolean isFinal() {
|
|
||||||
return this == VALIDE || this == REFUSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si ce statut nécessite une action admin
|
|
||||||
*/
|
|
||||||
public boolean requiresAdminAction() {
|
|
||||||
return this == EN_ATTENTE || this == EN_COURS_VERIFICATION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.dto.request;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import jakarta.validation.constraints.Positive;
|
|
||||||
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 CreateFolderRequest {
|
|
||||||
|
|
||||||
@NotNull(message = "Quote ID is required")
|
|
||||||
@Positive(message = "Quote ID must be positive")
|
|
||||||
Long quoteId;
|
|
||||||
|
|
||||||
@Size(max = 1000, message = "Comments must not exceed 1000 characters")
|
|
||||||
String commentairesClient;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,87 +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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public java.util.List<UserAccount> findAllUsers(int page, int size) {
|
|
||||||
return userRepository.findAllUsers(page, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public java.util.List<UserAccount> findUsersByCompany(UUID companyId, int page, int size) {
|
|
||||||
return userRepository.findUsersByCompany(companyId, page, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long countAllUsers() {
|
|
||||||
return userRepository.countAllUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long countUsersByCompany(UUID companyId) {
|
|
||||||
return userRepository.countUsersByCompany(companyId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
private LicenseRepository licenseRepository;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
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
|
@Test
|
||||||
@DisplayName("Should create company successfully")
|
void test(){
|
||||||
void shouldCreateCompanySuccessfully() {
|
int test = 1 +1;
|
||||||
// Given
|
assertEquals(2,test);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
private UserServiceImpl userService;
|
|
||||||
|
|
||||||
private UserAccount testUserAccount;
|
|
||||||
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
|
@Test
|
||||||
@DisplayName("Should delegate password change to repository")
|
void test(){
|
||||||
void shouldDelegatePasswordChangeToRepository() {
|
int test = 1 +1;
|
||||||
// When
|
assertEquals(2,test);
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
@ -1,42 +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);
|
|
||||||
|
|
||||||
List<UserAccount> findAllUsers(int page, int size);
|
|
||||||
|
|
||||||
List<UserAccount> findUsersByCompany(UUID companyId, int page, int size);
|
|
||||||
|
|
||||||
long countAllUsers();
|
|
||||||
|
|
||||||
long countUsersByCompany(UUID companyId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.entity;
|
|
||||||
|
|
||||||
public enum AuthProviderEntity {
|
|
||||||
LOCAL,
|
|
||||||
GOOGLE
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,148 +0,0 @@
|
|||||||
package com.dh7789dev.xpeditis.entity;
|
|
||||||
|
|
||||||
import com.dh7789dev.xpeditis.dto.app.StatutVerification;
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.experimental.FieldDefaults;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "documents_dossier")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class DocumentDossierEntity {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
Long id;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
|
||||||
@JoinColumn(name = "dossier_export_id", nullable = false)
|
|
||||||
ExportFolderEntity dossier;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
|
||||||
@JoinColumn(name = "type_document_id", nullable = false)
|
|
||||||
DocumentTypeEntity typeDocument;
|
|
||||||
|
|
||||||
// ========== FILE INFORMATION ==========
|
|
||||||
@Column(name = "nom_original", nullable = false, length = 255)
|
|
||||||
String nomOriginal;
|
|
||||||
|
|
||||||
@Column(name = "nom_stockage", nullable = false, length = 100)
|
|
||||||
String nomStockage; // UUID + extension
|
|
||||||
|
|
||||||
@Column(name = "chemin_stockage", nullable = false, length = 500)
|
|
||||||
String cheminStockage; // Path on filesystem
|
|
||||||
|
|
||||||
@Column(name = "taille_octets", nullable = false)
|
|
||||||
Long tailleOctets;
|
|
||||||
|
|
||||||
@Column(name = "type_mime", nullable = false, length = 100)
|
|
||||||
String typeMime;
|
|
||||||
|
|
||||||
@Column(name = "hash_fichier", length = 64)
|
|
||||||
String hashFichier; // SHA-256
|
|
||||||
|
|
||||||
// ========== VERSIONING ==========
|
|
||||||
@Column(name = "numero_version", nullable = false)
|
|
||||||
Integer numeroVersion = 1;
|
|
||||||
|
|
||||||
@Column(name = "description", length = 500)
|
|
||||||
String description;
|
|
||||||
|
|
||||||
@Column(name = "date_validite")
|
|
||||||
LocalDate dateValidite;
|
|
||||||
|
|
||||||
// ========== VERIFICATION STATUS ==========
|
|
||||||
@Enumerated(EnumType.STRING)
|
|
||||||
@Column(name = "statut_verification", nullable = false, length = 30)
|
|
||||||
StatutVerification statutVerification = StatutVerification.EN_ATTENTE;
|
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
|
||||||
@JoinColumn(name = "verifie_par")
|
|
||||||
UserEntity verifiePar;
|
|
||||||
|
|
||||||
@Column(name = "date_verification")
|
|
||||||
LocalDateTime dateVerification;
|
|
||||||
|
|
||||||
@Lob
|
|
||||||
@Column(name = "commentaire_verification")
|
|
||||||
String commentaireVerification;
|
|
||||||
|
|
||||||
@Lob
|
|
||||||
@Column(name = "corrections_demandees")
|
|
||||||
String correctionsDemandees;
|
|
||||||
|
|
||||||
// ========== METADATA ==========
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
|
||||||
@JoinColumn(name = "uploade_par", nullable = false)
|
|
||||||
UserEntity uploadePar;
|
|
||||||
|
|
||||||
@Column(name = "date_upload", nullable = false)
|
|
||||||
LocalDateTime dateUpload;
|
|
||||||
|
|
||||||
@Column(name = "updated_at")
|
|
||||||
LocalDateTime updatedAt;
|
|
||||||
|
|
||||||
@PrePersist
|
|
||||||
protected void onCreate() {
|
|
||||||
if (dateUpload == null) {
|
|
||||||
dateUpload = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreUpdate
|
|
||||||
protected void onUpdate() {
|
|
||||||
updatedAt = LocalDateTime.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== BUSINESS METHODS ==========
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si le document est valide (approuvé)
|
|
||||||
*/
|
|
||||||
public boolean isValid() {
|
|
||||||
return statutVerification == StatutVerification.VALIDE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si le document nécessite une action admin
|
|
||||||
*/
|
|
||||||
public boolean requiresAdminAction() {
|
|
||||||
return statutVerification.requiresAdminAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si le document peut être re-vérifié
|
|
||||||
*/
|
|
||||||
public boolean canBeReVerified() {
|
|
||||||
return statutVerification.allowsReVerification();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si le document a expiré
|
|
||||||
*/
|
|
||||||
public boolean isExpired() {
|
|
||||||
return dateValidite != null && dateValidite.isBefore(LocalDate.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un nom de fichier de stockage unique
|
|
||||||
*/
|
|
||||||
public static String generateStorageName(String originalName) {
|
|
||||||
String extension = "";
|
|
||||||
int i = originalName.lastIndexOf('.');
|
|
||||||
if (i > 0) {
|
|
||||||
extension = originalName.substring(i);
|
|
||||||
}
|
|
||||||
return java.util.UUID.randomUUID().toString() + extension;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +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;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "document_types")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
|
||||||
public class DocumentTypeEntity extends BaseEntity {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
Long id;
|
|
||||||
|
|
||||||
@Column(name = "nom", nullable = false, length = 100)
|
|
||||||
String nom;
|
|
||||||
|
|
||||||
@Column(name = "description", length = 500)
|
|
||||||
String description;
|
|
||||||
|
|
||||||
@Column(name = "obligatoire", nullable = false)
|
|
||||||
Boolean obligatoire = false;
|
|
||||||
|
|
||||||
@Column(name = "pour_import", nullable = false)
|
|
||||||
Boolean pourImport = false;
|
|
||||||
|
|
||||||
@Column(name = "pour_export", nullable = false)
|
|
||||||
Boolean pourExport = false;
|
|
||||||
|
|
||||||
@Column(name = "pour_marchandise_dangereuse", nullable = false)
|
|
||||||
Boolean pourMarchandiseDangereuse = false;
|
|
||||||
|
|
||||||
@Column(name = "extensions_acceptees", length = 200)
|
|
||||||
String extensionsAcceptees;
|
|
||||||
|
|
||||||
@Column(name = "taille_max_mo")
|
|
||||||
Integer tailleMaxMo = 10;
|
|
||||||
|
|
||||||
@Column(name = "ordre_affichage")
|
|
||||||
Integer ordreAffichage = 0;
|
|
||||||
|
|
||||||
@Column(name = "actif", nullable = false)
|
|
||||||
Boolean actif = true;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user