Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da8da492d2 | ||
|
|
b3ed387197 | ||
|
|
f31f1b6c69 | ||
|
|
d1be066a20 | ||
|
|
6b832ab4ca | ||
|
|
f86389cb84 | ||
|
|
cb2dcf4d3a | ||
|
|
30ddb9b631 | ||
|
|
c4356adcb2 |
166
.env.example
Normal file
166
.env.example
Normal file
@ -0,0 +1,166 @@
|
||||
# ===========================================
|
||||
# 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
|
||||
@ -1,105 +0,0 @@
|
||||
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 }}
|
||||
@ -1,94 +0,0 @@
|
||||
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
Normal file
300
CLAUDE.md
Normal file
@ -0,0 +1,300 @@
|
||||
# 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
Normal file
155
ENV_SETUP.md
Normal file
@ -0,0 +1,155 @@
|
||||
# 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
|
||||
```
|
||||
@ -1,7 +0,0 @@
|
||||
# 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>
|
||||
983
Xpeditis_API_Collection.postman_collection.json
Normal file
983
Xpeditis_API_Collection.postman_collection.json
Normal file
@ -0,0 +1,983 @@
|
||||
{
|
||||
"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,6 +29,14 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</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>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
@ -0,0 +1,317 @@
|
||||
package com.dh7789dev.xpeditis.controller;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.SubscriptionDto;
|
||||
import com.dh7789dev.xpeditis.dto.request.CreateSubscriptionRequest;
|
||||
import com.dh7789dev.xpeditis.dto.request.UpdateSubscriptionRequest;
|
||||
import com.dh7789dev.xpeditis.port.in.SubscriptionService;
|
||||
import com.dh7789dev.xpeditis.mapper.SubscriptionDtoMapper;
|
||||
import com.dh7789dev.xpeditis.dto.app.Subscription;
|
||||
|
||||
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.responses.ApiResponses;
|
||||
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.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Contrôleur REST pour la gestion des abonnements
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/subscriptions")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Subscriptions", description = "API de gestion des abonnements Stripe")
|
||||
public class SubscriptionController {
|
||||
|
||||
private final SubscriptionService subscriptionService;
|
||||
private final SubscriptionDtoMapper dtoMapper;
|
||||
|
||||
// ===== CRÉATION D'ABONNEMENT =====
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasRole('ADMIN') or hasRole('COMPANY_ADMIN')")
|
||||
@Operation(summary = "Créer un nouvel abonnement")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "201", description = "Abonnement créé avec succès"),
|
||||
@ApiResponse(responseCode = "400", description = "Données invalides"),
|
||||
@ApiResponse(responseCode = "403", description = "Accès refusé"),
|
||||
@ApiResponse(responseCode = "409", description = "Abonnement déjà existant")
|
||||
})
|
||||
public ResponseEntity<SubscriptionDto> createSubscription(
|
||||
@Valid @RequestBody CreateSubscriptionRequest request) {
|
||||
|
||||
log.info("Création d'abonnement pour l'entreprise {}, plan {}",
|
||||
request.getCompanyId(), request.getPlanType());
|
||||
|
||||
try {
|
||||
Subscription subscription = subscriptionService.createSubscription(
|
||||
request.getCompanyId(),
|
||||
request.getPlanType(),
|
||||
request.getBillingCycle(),
|
||||
request.getPaymentMethodId(),
|
||||
request.getStartTrial(),
|
||||
request.getCustomTrialDays()
|
||||
);
|
||||
|
||||
SubscriptionDto dto = dtoMapper.toDto(subscription);
|
||||
log.info("Abonnement créé avec succès: {}", subscription.getStripeSubscriptionId());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(dto);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la création de l'abonnement: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CONSULTATION DES ABONNEMENTS =====
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Operation(summary = "Lister tous les abonnements")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Liste des abonnements"),
|
||||
@ApiResponse(responseCode = "403", description = "Accès refusé")
|
||||
})
|
||||
public ResponseEntity<Page<SubscriptionDto.Summary>> getAllSubscriptions(
|
||||
@Parameter(description = "Statut de l'abonnement") @RequestParam(required = false) String status,
|
||||
@Parameter(description = "Cycle de facturation") @RequestParam(required = false) String billingCycle,
|
||||
@Parameter(description = "ID du client Stripe") @RequestParam(required = false) String customerId,
|
||||
Pageable pageable) {
|
||||
|
||||
log.debug("Récupération des abonnements - statut: {}, cycle: {}, client: {}",
|
||||
status, billingCycle, customerId);
|
||||
|
||||
Page<Subscription> subscriptions = subscriptionService.findSubscriptions(
|
||||
status, billingCycle, customerId, pageable);
|
||||
|
||||
List<SubscriptionDto.Summary> dtos = subscriptions.getContent().stream()
|
||||
.map(dtoMapper::toSummaryDto)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Page<SubscriptionDto.Summary> result = new PageImpl<>(dtos, pageable, subscriptions.getTotalElements());
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{subscriptionId}")
|
||||
@PreAuthorize("hasRole('ADMIN') or (hasRole('COMPANY_ADMIN') and @subscriptionSecurity.canAccessSubscription(authentication, #subscriptionId))")
|
||||
@Operation(summary = "Obtenir les détails d'un abonnement")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Détails de l'abonnement"),
|
||||
@ApiResponse(responseCode = "403", description = "Accès refusé"),
|
||||
@ApiResponse(responseCode = "404", description = "Abonnement non trouvé")
|
||||
})
|
||||
public ResponseEntity<SubscriptionDto.Detailed> getSubscription(
|
||||
@Parameter(description = "ID de l'abonnement") @PathVariable UUID subscriptionId) {
|
||||
|
||||
log.debug("Récupération de l'abonnement {}", subscriptionId);
|
||||
|
||||
Optional<Subscription> subscription = subscriptionService.findById(subscriptionId);
|
||||
|
||||
if (subscription.isEmpty()) {
|
||||
log.warn("Abonnement {} non trouvé", subscriptionId);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
SubscriptionDto.Detailed dto = dtoMapper.toDetailedDto(subscription.get());
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
// ===== MODIFICATION D'ABONNEMENT =====
|
||||
|
||||
@PutMapping("/{subscriptionId}")
|
||||
@PreAuthorize("hasRole('ADMIN') or (hasRole('COMPANY_ADMIN') and @subscriptionSecurity.canAccessSubscription(authentication, #subscriptionId))")
|
||||
@Operation(summary = "Modifier un abonnement")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Abonnement modifié"),
|
||||
@ApiResponse(responseCode = "400", description = "Données invalides"),
|
||||
@ApiResponse(responseCode = "403", description = "Accès refusé"),
|
||||
@ApiResponse(responseCode = "404", description = "Abonnement non trouvé")
|
||||
})
|
||||
public ResponseEntity<SubscriptionDto> updateSubscription(
|
||||
@Parameter(description = "ID de l'abonnement") @PathVariable UUID subscriptionId,
|
||||
@Valid @RequestBody UpdateSubscriptionRequest request) {
|
||||
|
||||
log.info("Modification de l'abonnement {} - nouveau plan: {}",
|
||||
subscriptionId, request.getNewPlanType());
|
||||
|
||||
try {
|
||||
Optional<Subscription> updated = Optional.empty();
|
||||
|
||||
if (request.getNewPlanType() != null || request.getNewBillingCycle() != null) {
|
||||
updated = Optional.of(subscriptionService.changeSubscriptionPlan(
|
||||
subscriptionId,
|
||||
request.getNewPlanType(),
|
||||
request.getNewBillingCycle(),
|
||||
request.getEnableProration(),
|
||||
request.getImmediateChange()
|
||||
));
|
||||
}
|
||||
|
||||
if (request.getNewPaymentMethodId() != null) {
|
||||
subscriptionService.updatePaymentMethod(subscriptionId, request.getNewPaymentMethodId());
|
||||
if (updated.isEmpty()) {
|
||||
updated = subscriptionService.findById(subscriptionId);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.getCancelAtPeriodEnd() != null) {
|
||||
if (request.getCancelAtPeriodEnd()) {
|
||||
updated = Optional.of(subscriptionService.scheduleForCancellation(
|
||||
subscriptionId, request.getCancellationReason()));
|
||||
} else {
|
||||
updated = Optional.of(subscriptionService.reactivateSubscription(subscriptionId));
|
||||
}
|
||||
}
|
||||
|
||||
if (updated.isEmpty()) {
|
||||
log.warn("Aucune modification apportée à l'abonnement {}", subscriptionId);
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
SubscriptionDto dto = dtoMapper.toDto(updated.get());
|
||||
log.info("Abonnement {} modifié avec succès", subscriptionId);
|
||||
|
||||
return ResponseEntity.ok(dto);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la modification de l'abonnement {}: {}", subscriptionId, e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ACTIONS SPÉCIFIQUES =====
|
||||
|
||||
@PostMapping("/{subscriptionId}/cancel")
|
||||
@PreAuthorize("hasRole('ADMIN') or (hasRole('COMPANY_ADMIN') and @subscriptionSecurity.canAccessSubscription(authentication, #subscriptionId))")
|
||||
@Operation(summary = "Annuler un abonnement immédiatement")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Abonnement annulé"),
|
||||
@ApiResponse(responseCode = "403", description = "Accès refusé"),
|
||||
@ApiResponse(responseCode = "404", description = "Abonnement non trouvé")
|
||||
})
|
||||
public ResponseEntity<SubscriptionDto> cancelSubscriptionImmediately(
|
||||
@Parameter(description = "ID de l'abonnement") @PathVariable UUID subscriptionId,
|
||||
@Parameter(description = "Raison de l'annulation") @RequestParam(required = false) String reason) {
|
||||
|
||||
log.info("Annulation immédiate de l'abonnement {} - raison: {}", subscriptionId, reason);
|
||||
|
||||
try {
|
||||
Subscription subscription = subscriptionService.cancelSubscriptionImmediately(subscriptionId, reason);
|
||||
SubscriptionDto dto = dtoMapper.toDto(subscription);
|
||||
|
||||
log.info("Abonnement {} annulé avec succès", subscriptionId);
|
||||
return ResponseEntity.ok(dto);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'annulation de l'abonnement {}: {}", subscriptionId, e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{subscriptionId}/reactivate")
|
||||
@PreAuthorize("hasRole('ADMIN') or (hasRole('COMPANY_ADMIN') and @subscriptionSecurity.canAccessSubscription(authentication, #subscriptionId))")
|
||||
@Operation(summary = "Réactiver un abonnement annulé")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Abonnement réactivé"),
|
||||
@ApiResponse(responseCode = "403", description = "Accès refusé"),
|
||||
@ApiResponse(responseCode = "404", description = "Abonnement non trouvé"),
|
||||
@ApiResponse(responseCode = "409", description = "Abonnement ne peut pas être réactivé")
|
||||
})
|
||||
public ResponseEntity<SubscriptionDto> reactivateSubscription(
|
||||
@Parameter(description = "ID de l'abonnement") @PathVariable UUID subscriptionId) {
|
||||
|
||||
log.info("Réactivation de l'abonnement {}", subscriptionId);
|
||||
|
||||
try {
|
||||
Subscription subscription = subscriptionService.reactivateSubscription(subscriptionId);
|
||||
SubscriptionDto dto = dtoMapper.toDto(subscription);
|
||||
|
||||
log.info("Abonnement {} réactivé avec succès", subscriptionId);
|
||||
return ResponseEntity.ok(dto);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la réactivation de l'abonnement {}: {}", subscriptionId, e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== RECHERCHES SPÉCIALISÉES =====
|
||||
|
||||
@GetMapping("/company/{companyId}")
|
||||
@PreAuthorize("hasRole('ADMIN') or (hasRole('COMPANY_ADMIN') and @companySecurity.canAccessCompany(authentication, #companyId))")
|
||||
@Operation(summary = "Obtenir les abonnements d'une entreprise")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Abonnements de l'entreprise"),
|
||||
@ApiResponse(responseCode = "403", description = "Accès refusé")
|
||||
})
|
||||
public ResponseEntity<List<SubscriptionDto.Summary>> getCompanySubscriptions(
|
||||
@Parameter(description = "ID de l'entreprise") @PathVariable UUID companyId) {
|
||||
|
||||
log.debug("Récupération des abonnements de l'entreprise {}", companyId);
|
||||
|
||||
List<Subscription> subscriptions = subscriptionService.findByCompanyId(companyId);
|
||||
List<SubscriptionDto.Summary> dtos = subscriptions.stream()
|
||||
.map(dtoMapper::toSummaryDto)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@GetMapping("/requiring-attention")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Operation(summary = "Obtenir les abonnements nécessitant une attention")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Abonnements nécessitant une attention"),
|
||||
@ApiResponse(responseCode = "403", description = "Accès refusé")
|
||||
})
|
||||
public ResponseEntity<List<SubscriptionDto.Summary>> getSubscriptionsRequiringAttention() {
|
||||
|
||||
log.debug("Récupération des abonnements nécessitant une attention");
|
||||
|
||||
List<Subscription> subscriptions = subscriptionService.findSubscriptionsRequiringAttention();
|
||||
List<SubscriptionDto.Summary> dtos = subscriptions.stream()
|
||||
.map(dtoMapper::toSummaryDto)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
@GetMapping("/trial/ending-soon")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@Operation(summary = "Obtenir les abonnements en essai qui se terminent bientôt")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "Essais se terminant bientôt"),
|
||||
@ApiResponse(responseCode = "403", description = "Accès refusé")
|
||||
})
|
||||
public ResponseEntity<List<SubscriptionDto.Summary>> getTrialsEndingSoon(
|
||||
@Parameter(description = "Nombre de jours d'avance") @RequestParam(defaultValue = "7") int daysAhead) {
|
||||
|
||||
log.debug("Récupération des essais se terminant dans {} jours", daysAhead);
|
||||
|
||||
List<Subscription> subscriptions = subscriptionService.findTrialsEndingSoon(daysAhead);
|
||||
List<SubscriptionDto.Summary> dtos = subscriptions.stream()
|
||||
.map(dtoMapper::toSummaryDto)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
}
|
||||
@ -2,35 +2,101 @@ package com.dh7789dev.xpeditis.controller.api.v1;
|
||||
|
||||
import com.dh7789dev.xpeditis.AuthenticationService;
|
||||
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||
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.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.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping(value = "${apiPrefix}/api/v1/auth",
|
||||
produces = APPLICATION_JSON_VALUE)
|
||||
@RequestMapping(value = "${apiPrefix}/api/v1/auth", produces = APPLICATION_JSON_VALUE)
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Authentication", description = "Authentication and registration endpoints")
|
||||
public class AuthenticationRestController {
|
||||
|
||||
private final AuthenticationService service;
|
||||
private final AuthenticationService authenticationService;
|
||||
|
||||
@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")
|
||||
public ResponseEntity<AuthenticationResponse> authenticate(
|
||||
@RequestBody @Valid AuthenticationRequest request) {
|
||||
return ResponseEntity.ok(service.authenticate(request));
|
||||
log.info("Authentication attempt for user: {}", request.getUsername());
|
||||
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")
|
||||
public ResponseEntity<AuthenticationResponse> register(
|
||||
@RequestBody @Valid RegisterRequest request) {
|
||||
return ResponseEntity.ok(service.register(request));
|
||||
log.info("Registration attempt for email: {}", request.getEmail());
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,363 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,274 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,358 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
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,38 +1,153 @@
|
||||
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.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.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.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
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 org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@Validated
|
||||
@RequestMapping(value = "${apiPrefix}/api/v1/users",
|
||||
produces = APPLICATION_JSON_VALUE)
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping(value = "${apiPrefix}/api/v1/users", produces = APPLICATION_JSON_VALUE)
|
||||
@Tag(name = "User Management", description = "User profile and management endpoints")
|
||||
public class UserRestController {
|
||||
|
||||
private final UserService service;
|
||||
private final UserService userService;
|
||||
|
||||
public UserRestController(UserService service) {
|
||||
this.service = service;
|
||||
@Operation(summary = "Get current user profile", description = "Retrieve the profile of the authenticated user")
|
||||
@ApiResponse(responseCode = "200", description = "User profile retrieved successfully")
|
||||
@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 = "Change password of the connected user")
|
||||
@PatchMapping("/password")
|
||||
@Operation(summary = "Update user profile", description = "Update profile information of the authenticated user")
|
||||
@ApiResponse(responseCode = "200", description = "Profile updated successfully")
|
||||
@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(
|
||||
@RequestBody ChangePasswordRequest request,
|
||||
@RequestBody @Valid ChangePasswordRequest request,
|
||||
Principal connectedUser) {
|
||||
service.changePassword(request, connectedUser);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
log.info("Password change request for user: {}", connectedUser.getName());
|
||||
userService.changePassword(request, connectedUser);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Operation(summary = "Delete user account", description = "Delete the authenticated user's account")
|
||||
@ApiResponse(responseCode = "204", description = "Account deleted successfully")
|
||||
@DeleteMapping("/account")
|
||||
public ResponseEntity<Void> deleteAccount(Authentication authentication) {
|
||||
log.info("Account deletion request for user: {}", authentication.getName());
|
||||
|
||||
UserAccount user = userService.findByUsername(authentication.getName())
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
|
||||
userService.deleteUser(user.getId());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Operation(summary = "List users", description = "List all users (Admin only)")
|
||||
@ApiResponse(responseCode = "200", description = "Users retrieved successfully")
|
||||
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
|
||||
@GetMapping
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<Page<UserResponse>> listUsers(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) UUID companyId) {
|
||||
log.info("User list request - page: {}, size: {}, companyId: {}", page, size, companyId);
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size);
|
||||
|
||||
// TODO: Implement pagination and company filtering in service layer
|
||||
// For now, return an empty page
|
||||
Page<UserResponse> users = Page.empty(pageable);
|
||||
|
||||
return ResponseEntity.ok(users);
|
||||
}
|
||||
|
||||
// Helper method to convert UserAccount to UserResponse
|
||||
private UserResponse mapToUserResponse(UserAccount userAccount) {
|
||||
return UserResponse.builder()
|
||||
.id(userAccount.getId())
|
||||
.firstName(userAccount.getFirstName())
|
||||
.lastName(userAccount.getLastName())
|
||||
.email(userAccount.getEmail() != null ? userAccount.getEmail().getValue() : null)
|
||||
.username(userAccount.getUsername())
|
||||
.phoneNumber(userAccount.getPhoneNumber() != null ? userAccount.getPhoneNumber().getValue() : null)
|
||||
.authProvider(userAccount.getAuthProvider())
|
||||
.privacyPolicyAccepted(userAccount.isPrivacyPolicyAccepted())
|
||||
.privacyPolicyAcceptedAt(userAccount.getPrivacyPolicyAcceptedAt())
|
||||
.createdAt(userAccount.getCreatedAt())
|
||||
.lastLoginAt(userAccount.getLastLoginAt())
|
||||
.isActive(userAccount.isActive())
|
||||
.role(userAccount.getRole() != null ? userAccount.getRole().name() : null)
|
||||
.companyName(userAccount.getCompany() != null ? userAccount.getCompany().getName() : null)
|
||||
.companyId(userAccount.getCompany() != null ? userAccount.getCompany().getId() : null)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,247 @@
|
||||
package com.dh7789dev.xpeditis.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour les factures dans les réponses API
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class InvoiceDto {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "L'ID Stripe de la facture est obligatoire")
|
||||
private String stripeInvoiceId;
|
||||
|
||||
@NotBlank(message = "Le numéro de facture est obligatoire")
|
||||
@Size(max = 50, message = "Le numéro de facture ne peut pas dépasser 50 caractères")
|
||||
private String invoiceNumber;
|
||||
|
||||
private UUID subscriptionId;
|
||||
|
||||
@NotNull(message = "Le statut est obligatoire")
|
||||
private String status;
|
||||
|
||||
@NotNull(message = "Le montant dû est obligatoire")
|
||||
@DecimalMin(value = "0.0", message = "Le montant dû doit être positif")
|
||||
@Digits(integer = 8, fraction = 2, message = "Le montant dû doit avoir au maximum 8 chiffres avant la virgule et 2 après")
|
||||
private BigDecimal amountDue;
|
||||
|
||||
@DecimalMin(value = "0.0", message = "Le montant payé doit être positif")
|
||||
@Digits(integer = 8, fraction = 2, message = "Le montant payé doit avoir au maximum 8 chiffres avant la virgule et 2 après")
|
||||
private BigDecimal amountPaid = BigDecimal.ZERO;
|
||||
|
||||
@NotBlank(message = "La devise est obligatoire")
|
||||
@Size(min = 3, max = 3, message = "La devise doit faire exactement 3 caractères")
|
||||
private String currency = "EUR";
|
||||
|
||||
@NotNull(message = "Le début de période de facturation est obligatoire")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime billingPeriodStart;
|
||||
|
||||
@NotNull(message = "La fin de période de facturation est obligatoire")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime billingPeriodEnd;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime dueDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime paidAt;
|
||||
|
||||
private String invoicePdfUrl;
|
||||
private String hostedInvoiceUrl;
|
||||
|
||||
@Min(value = 0, message = "Le nombre de tentatives doit être positif")
|
||||
private Integer attemptCount = 0;
|
||||
|
||||
// Relations
|
||||
private List<InvoiceLineItemDto> lineItems;
|
||||
|
||||
// Métadonnées
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
// ===== PROPRIÉTÉS CALCULÉES =====
|
||||
|
||||
/**
|
||||
* @return true si la facture est payée
|
||||
*/
|
||||
public Boolean isPaid() {
|
||||
return "PAID".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est en attente de paiement
|
||||
*/
|
||||
public Boolean isPending() {
|
||||
return "OPEN".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est en retard
|
||||
*/
|
||||
public Boolean isOverdue() {
|
||||
return dueDate != null &&
|
||||
LocalDateTime.now().isAfter(dueDate) &&
|
||||
!isPaid();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours de retard (0 si pas en retard)
|
||||
*/
|
||||
public Long getDaysOverdue() {
|
||||
if (!isOverdue()) return 0L;
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(dueDate, LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le montant restant à payer
|
||||
*/
|
||||
public BigDecimal getRemainingAmount() {
|
||||
if (amountPaid == null) return amountDue;
|
||||
return amountDue.subtract(amountPaid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est partiellement payée
|
||||
*/
|
||||
public Boolean isPartiallyPaid() {
|
||||
return amountPaid != null &&
|
||||
amountPaid.compareTo(BigDecimal.ZERO) > 0 &&
|
||||
amountPaid.compareTo(amountDue) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le pourcentage de paiement effectué
|
||||
*/
|
||||
public Double getPaymentPercentage() {
|
||||
if (amountDue.compareTo(BigDecimal.ZERO) == 0) return 100.0;
|
||||
if (amountPaid == null) return 0.0;
|
||||
|
||||
return amountPaid.divide(amountDue, 4, java.math.RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100))
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return la durée de la période de facturation en jours
|
||||
*/
|
||||
public Long getBillingPeriodDays() {
|
||||
if (billingPeriodStart == null || billingPeriodEnd == null) return null;
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(billingPeriodStart, billingPeriodEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette facture nécessite une attention
|
||||
*/
|
||||
public Boolean requiresAttention() {
|
||||
return isOverdue() ||
|
||||
(attemptCount != null && attemptCount > 3) ||
|
||||
"PAYMENT_FAILED".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture inclut du prorata
|
||||
*/
|
||||
public Boolean hasProrationItems() {
|
||||
return lineItems != null &&
|
||||
lineItems.stream().anyMatch(item ->
|
||||
item.getProrated() != null && item.getProrated());
|
||||
}
|
||||
|
||||
// ===== SOUS-CLASSES POUR RESPONSES SPÉCIFIQUES =====
|
||||
|
||||
/**
|
||||
* Version minimale pour les listes
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Summary {
|
||||
private UUID id;
|
||||
private String invoiceNumber;
|
||||
private String status;
|
||||
private BigDecimal amountDue;
|
||||
private BigDecimal amountPaid;
|
||||
private String currency;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime dueDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private Boolean isOverdue;
|
||||
private Boolean requiresAttention;
|
||||
private Long daysOverdue;
|
||||
private BigDecimal remainingAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version détaillée pour les vues individuelles
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Detailed extends InvoiceDto {
|
||||
// Informations sur l'abonnement
|
||||
private String subscriptionPlan;
|
||||
private String companyName;
|
||||
|
||||
// Actions disponibles
|
||||
private Boolean canDownloadPdf;
|
||||
private Boolean canPayOnline;
|
||||
private Boolean canSendReminder;
|
||||
|
||||
// Historique des paiements
|
||||
private List<PaymentAttemptDto> paymentAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version publique pour les clients (sans données sensibles)
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Customer {
|
||||
private String invoiceNumber;
|
||||
private String status;
|
||||
private BigDecimal amountDue;
|
||||
private BigDecimal amountPaid;
|
||||
private String currency;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime dueDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime paidAt;
|
||||
|
||||
private String hostedInvoiceUrl;
|
||||
private List<InvoiceLineItemDto> lineItems;
|
||||
private BigDecimal remainingAmount;
|
||||
private Boolean isOverdue;
|
||||
private Long daysOverdue;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO pour les tentatives de paiement
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class PaymentAttemptDto {
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime attemptDate;
|
||||
|
||||
private String status;
|
||||
private String failureReason;
|
||||
private BigDecimal attemptedAmount;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,187 @@
|
||||
package com.dh7789dev.xpeditis.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour les lignes de facture dans les réponses API
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class InvoiceLineItemDto {
|
||||
|
||||
private UUID id;
|
||||
private UUID invoiceId;
|
||||
|
||||
@Size(max = 1000, message = "La description ne peut pas dépasser 1000 caractères")
|
||||
private String description;
|
||||
|
||||
@Min(value = 1, message = "La quantité doit être au minimum 1")
|
||||
private Integer quantity = 1;
|
||||
|
||||
@DecimalMin(value = "0.0", message = "Le prix unitaire doit être positif")
|
||||
@Digits(integer = 8, fraction = 2, message = "Le prix unitaire doit avoir au maximum 8 chiffres avant la virgule et 2 après")
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
@NotNull(message = "Le montant est obligatoire")
|
||||
@DecimalMin(value = "0.0", message = "Le montant doit être positif")
|
||||
@Digits(integer = 8, fraction = 2, message = "Le montant doit avoir au maximum 8 chiffres avant la virgule et 2 après")
|
||||
private BigDecimal amount;
|
||||
|
||||
private String stripePriceId;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime periodStart;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime periodEnd;
|
||||
|
||||
private Boolean prorated = false;
|
||||
|
||||
// Métadonnées
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
// ===== PROPRIÉTÉS CALCULÉES =====
|
||||
|
||||
/**
|
||||
* @return true si cette ligne représente un prorata
|
||||
*/
|
||||
public Boolean isProrationItem() {
|
||||
return prorated != null && prorated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return la durée couverte par cette ligne en jours
|
||||
*/
|
||||
public Long getPeriodDays() {
|
||||
if (periodStart == null || periodEnd == null) {
|
||||
return null;
|
||||
}
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(periodStart, periodEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le prix unitaire calculé (amount / quantity)
|
||||
*/
|
||||
public BigDecimal getCalculatedUnitPrice() {
|
||||
if (quantity == null || quantity == 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return amount.divide(BigDecimal.valueOf(quantity), 2, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette ligne couvre une période complète (mois ou année)
|
||||
*/
|
||||
public Boolean isFullPeriodItem() {
|
||||
if (periodStart == null || periodEnd == null || isProrationItem()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Long days = getPeriodDays();
|
||||
if (days == null) return false;
|
||||
|
||||
// Vérifier si c'est approximativement un mois (28-31 jours) ou une année (365-366 jours)
|
||||
return (days >= 28 && days <= 31) || (days >= 365 && days <= 366);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le type de période (MONTH, YEAR, PRORATION, CUSTOM)
|
||||
*/
|
||||
public String getPeriodType() {
|
||||
if (isProrationItem()) {
|
||||
return "PRORATION";
|
||||
}
|
||||
|
||||
Long days = getPeriodDays();
|
||||
if (days == null) return "UNKNOWN";
|
||||
|
||||
if (days >= 28 && days <= 31) {
|
||||
return "MONTH";
|
||||
} else if (days >= 365 && days <= 366) {
|
||||
return "YEAR";
|
||||
} else {
|
||||
return "CUSTOM";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le taux de prorata (pour les éléments proratés)
|
||||
*/
|
||||
public Double getProrationRate() {
|
||||
if (!isProrationItem() || periodStart == null || periodEnd == null) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
Long actualDays = getPeriodDays();
|
||||
if (actualDays == null) return 1.0;
|
||||
|
||||
long expectedDays = getPeriodType().equals("YEAR") ? 365 : 30;
|
||||
return actualDays.doubleValue() / expectedDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return une description formatée avec les détails de période
|
||||
*/
|
||||
public String getFormattedDescription() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (description != null) {
|
||||
sb.append(description);
|
||||
}
|
||||
|
||||
if (periodStart != null && periodEnd != null) {
|
||||
sb.append(" (")
|
||||
.append(periodStart.toLocalDate())
|
||||
.append(" - ")
|
||||
.append(periodEnd.toLocalDate())
|
||||
.append(")");
|
||||
}
|
||||
|
||||
if (isProrationItem()) {
|
||||
sb.append(" [Prorata]");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ===== SOUS-CLASSES POUR RESPONSES SPÉCIFIQUES =====
|
||||
|
||||
/**
|
||||
* Version minimale pour les résumés de facture
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Summary {
|
||||
private String description;
|
||||
private Integer quantity;
|
||||
private BigDecimal amount;
|
||||
private Boolean prorated;
|
||||
private String periodType;
|
||||
private String formattedDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version détaillée pour les analyses
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Detailed extends InvoiceLineItemDto {
|
||||
// Analyse de la période
|
||||
private Long periodDays;
|
||||
private Double prorationRate;
|
||||
private Boolean isFullPeriodItem;
|
||||
|
||||
// Comparaisons
|
||||
private BigDecimal calculatedUnitPrice;
|
||||
private String periodTypeDescription;
|
||||
private String formattedDescription;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,256 @@
|
||||
package com.dh7789dev.xpeditis.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour les méthodes de paiement dans les réponses API
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class PaymentMethodDto {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "L'ID Stripe de la méthode de paiement est obligatoire")
|
||||
private String stripePaymentMethodId;
|
||||
|
||||
@NotNull(message = "Le type de méthode de paiement est obligatoire")
|
||||
private String type;
|
||||
|
||||
private Boolean isDefault = false;
|
||||
|
||||
// Informations carte
|
||||
@Size(max = 50, message = "La marque de la carte ne peut pas dépasser 50 caractères")
|
||||
private String cardBrand;
|
||||
|
||||
@Size(min = 4, max = 4, message = "Les 4 derniers chiffres de la carte doivent faire exactement 4 caractères")
|
||||
private String cardLast4;
|
||||
|
||||
@Min(value = 1, message = "Le mois d'expiration doit être entre 1 et 12")
|
||||
@Max(value = 12, message = "Le mois d'expiration doit être entre 1 et 12")
|
||||
private Integer cardExpMonth;
|
||||
|
||||
@Min(value = 2020, message = "L'année d'expiration doit être valide")
|
||||
private Integer cardExpYear;
|
||||
|
||||
// Informations banque
|
||||
@Size(max = 100, message = "Le nom de la banque ne peut pas dépasser 100 caractères")
|
||||
private String bankName;
|
||||
|
||||
@NotNull(message = "L'ID de l'entreprise est obligatoire")
|
||||
private UUID companyId;
|
||||
|
||||
// Métadonnées
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
// ===== PROPRIÉTÉS CALCULÉES =====
|
||||
|
||||
/**
|
||||
* @return true si c'est une carte de crédit/débit
|
||||
*/
|
||||
public Boolean isCard() {
|
||||
return "CARD".equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un prélèvement bancaire SEPA
|
||||
*/
|
||||
public Boolean isSepaDebit() {
|
||||
return "SEPA_DEBIT".equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette méthode de paiement est la méthode par défaut
|
||||
*/
|
||||
public Boolean isDefaultPaymentMethod() {
|
||||
return isDefault != null && isDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la carte expire bientôt (dans les 2 prochains mois)
|
||||
*/
|
||||
public Boolean isCardExpiringSoon() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expirationDate = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1) // Le dernier jour du mois d'expiration
|
||||
.minusDays(1);
|
||||
|
||||
LocalDateTime twoMonthsFromNow = now.plusMonths(2);
|
||||
return expirationDate.isBefore(twoMonthsFromNow);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la carte est expirée
|
||||
*/
|
||||
public Boolean isCardExpired() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expirationDate = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1)
|
||||
.minusDays(1);
|
||||
|
||||
return expirationDate.isBefore(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours jusqu'à l'expiration (négatif si déjà expirée)
|
||||
*/
|
||||
public Long getDaysUntilExpiration() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expirationDate = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1)
|
||||
.minusDays(1);
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(now, expirationDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return une description formatée de la méthode de paiement
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
if (isCard()) {
|
||||
String brand = cardBrand != null ? cardBrand.toUpperCase() : "CARD";
|
||||
String last4 = cardLast4 != null ? cardLast4 : "****";
|
||||
return brand + " •••• " + last4;
|
||||
} else if (isSepaDebit()) {
|
||||
String bank = bankName != null ? bankName : "Bank";
|
||||
return "SEPA - " + bank;
|
||||
} else {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return les détails d'expiration formatés (MM/YY)
|
||||
*/
|
||||
public String getFormattedExpiration() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return String.format("%02d/%02d", cardExpMonth, cardExpYear % 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette méthode de paiement peut être utilisée pour des paiements récurrents
|
||||
*/
|
||||
public Boolean supportsRecurringPayments() {
|
||||
return "CARD".equals(type) || "SEPA_DEBIT".equals(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette méthode de paiement nécessite une attention
|
||||
*/
|
||||
public Boolean requiresAttention() {
|
||||
return isCardExpired() || isCardExpiringSoon();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return l'icône à afficher pour cette méthode de paiement
|
||||
*/
|
||||
public String getIconName() {
|
||||
if (isCard() && cardBrand != null) {
|
||||
return switch (cardBrand.toLowerCase()) {
|
||||
case "visa" -> "visa";
|
||||
case "mastercard" -> "mastercard";
|
||||
case "amex", "american_express" -> "amex";
|
||||
case "discover" -> "discover";
|
||||
case "diners", "diners_club" -> "diners";
|
||||
case "jcb" -> "jcb";
|
||||
case "unionpay" -> "unionpay";
|
||||
default -> "card";
|
||||
};
|
||||
} else if (isSepaDebit()) {
|
||||
return "bank";
|
||||
} else {
|
||||
return switch (type) {
|
||||
case "BANCONTACT" -> "bancontact";
|
||||
case "GIROPAY" -> "giropay";
|
||||
case "IDEAL" -> "ideal";
|
||||
default -> "payment";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== SOUS-CLASSES POUR RESPONSES SPÉCIFIQUES =====
|
||||
|
||||
/**
|
||||
* Version publique pour les clients (sans données sensibles)
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Public {
|
||||
private UUID id;
|
||||
private String type;
|
||||
private Boolean isDefault;
|
||||
private String cardBrand;
|
||||
private String cardLast4;
|
||||
private String formattedExpiration;
|
||||
private String bankName;
|
||||
private String displayName;
|
||||
private String iconName;
|
||||
private Boolean requiresAttention;
|
||||
private Boolean isCardExpiringSoon;
|
||||
private Boolean supportsRecurringPayments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version administrative complète
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Admin extends PaymentMethodDto {
|
||||
// Statistiques d'utilisation
|
||||
private Long totalTransactions;
|
||||
private Long successfulTransactions;
|
||||
private Long failedTransactions;
|
||||
private Double successRate;
|
||||
|
||||
// Dernière activité
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime lastUsed;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime lastSyncWithStripe;
|
||||
|
||||
// Alertes
|
||||
private Boolean needsAttention;
|
||||
private String attentionReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version minimal pour les listes
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Summary {
|
||||
private UUID id;
|
||||
private String type;
|
||||
private Boolean isDefault;
|
||||
private String displayName;
|
||||
private String iconName;
|
||||
private Boolean requiresAttention;
|
||||
private Long daysUntilExpiration;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
package com.dh7789dev.xpeditis.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour les plans d'abonnement dans les réponses API
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class PlanDto {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "Le nom du plan est obligatoire")
|
||||
@Size(max = 100, message = "Le nom du plan ne peut pas dépasser 100 caractères")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "Le type du plan est obligatoire")
|
||||
@Size(max = 50, message = "Le type du plan ne peut pas dépasser 50 caractères")
|
||||
private String type;
|
||||
|
||||
private String stripePriceIdMonthly;
|
||||
private String stripePriceIdYearly;
|
||||
|
||||
@DecimalMin(value = "0.0", message = "Le prix mensuel doit être positif")
|
||||
@Digits(integer = 8, fraction = 2, message = "Le prix mensuel doit avoir au maximum 8 chiffres avant la virgule et 2 après")
|
||||
private BigDecimal monthlyPrice;
|
||||
|
||||
@DecimalMin(value = "0.0", message = "Le prix annuel doit être positif")
|
||||
@Digits(integer = 8, fraction = 2, message = "Le prix annuel doit avoir au maximum 8 chiffres avant la virgule et 2 après")
|
||||
private BigDecimal yearlyPrice;
|
||||
|
||||
@NotNull(message = "Le nombre maximum d'utilisateurs est obligatoire")
|
||||
@Min(value = -1, message = "Le nombre maximum d'utilisateurs doit être -1 (illimité) ou positif")
|
||||
private Integer maxUsers;
|
||||
|
||||
private Set<String> features;
|
||||
|
||||
@Min(value = 0, message = "La durée d'essai doit être positive")
|
||||
private Integer trialDurationDays = 14;
|
||||
|
||||
private Boolean isActive = true;
|
||||
|
||||
@Min(value = 0, message = "L'ordre d'affichage doit être positif")
|
||||
private Integer displayOrder;
|
||||
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
// Métadonnées
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
// ===== PROPRIÉTÉS CALCULÉES =====
|
||||
|
||||
/**
|
||||
* @return true si le plan est disponible pour souscription
|
||||
*/
|
||||
public Boolean isAvailableForSubscription() {
|
||||
return isActive != null && isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si le plan supporte un nombre illimité d'utilisateurs
|
||||
*/
|
||||
public Boolean hasUnlimitedUsers() {
|
||||
return maxUsers != null && maxUsers == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de fonctionnalités incluses
|
||||
*/
|
||||
public Integer getFeatureCount() {
|
||||
return features != null ? features.size() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return l'économie annuelle (différence entre 12*mensuel et annuel)
|
||||
*/
|
||||
public BigDecimal getYearlySavings() {
|
||||
if (monthlyPrice == null || yearlyPrice == null) return null;
|
||||
|
||||
BigDecimal yearlyFromMonthly = monthlyPrice.multiply(BigDecimal.valueOf(12));
|
||||
return yearlyFromMonthly.subtract(yearlyPrice);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le pourcentage d'économie annuelle
|
||||
*/
|
||||
public Double getYearlySavingsPercentage() {
|
||||
BigDecimal savings = getYearlySavings();
|
||||
if (savings == null || monthlyPrice == null || monthlyPrice.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BigDecimal yearlyFromMonthly = monthlyPrice.multiply(BigDecimal.valueOf(12));
|
||||
return savings.divide(yearlyFromMonthly, 4, java.math.RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100))
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si ce plan est recommandé
|
||||
*/
|
||||
public Boolean isRecommended() {
|
||||
return metadata != null && Boolean.TRUE.equals(metadata.get("recommended"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si ce plan est populaire
|
||||
*/
|
||||
public Boolean isPopular() {
|
||||
return metadata != null && Boolean.TRUE.equals(metadata.get("popular"));
|
||||
}
|
||||
|
||||
// ===== SOUS-CLASSES POUR RESPONSES SPÉCIFIQUES =====
|
||||
|
||||
/**
|
||||
* Version publique pour les pages de pricing (sans IDs Stripe)
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Public {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private String type;
|
||||
private BigDecimal monthlyPrice;
|
||||
private BigDecimal yearlyPrice;
|
||||
private Integer maxUsers;
|
||||
private Set<String> features;
|
||||
private Integer trialDurationDays;
|
||||
private Integer displayOrder;
|
||||
private Boolean isRecommended;
|
||||
private Boolean isPopular;
|
||||
private BigDecimal yearlySavings;
|
||||
private Double yearlySavingsPercentage;
|
||||
private Boolean hasUnlimitedUsers;
|
||||
private Integer featureCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version administrative complète
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Admin extends PlanDto {
|
||||
// Statistiques d'utilisation
|
||||
private Long activeSubscriptions;
|
||||
private Long totalSubscriptions;
|
||||
private BigDecimal monthlyRecurringRevenue;
|
||||
private BigDecimal annualRecurringRevenue;
|
||||
|
||||
// Données de gestion
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime lastSyncWithStripe;
|
||||
private Boolean needsStripeSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version pour comparaison de plans
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Comparison {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private BigDecimal monthlyPrice;
|
||||
private BigDecimal yearlyPrice;
|
||||
private Integer maxUsers;
|
||||
private Set<String> features;
|
||||
private Boolean isRecommended;
|
||||
private Boolean isPopular;
|
||||
private Boolean hasUnlimitedUsers;
|
||||
private Integer featureCount;
|
||||
private BigDecimal yearlySavings;
|
||||
private Double yearlySavingsPercentage;
|
||||
|
||||
// Comparaison relative
|
||||
private String relativePricing; // "cheapest", "most-expensive", "mid-range"
|
||||
private Boolean bestValue;
|
||||
private String targetAudience; // "individual", "small-team", "enterprise"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
package com.dh7789dev.xpeditis.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour les abonnements dans les réponses API
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class SubscriptionDto {
|
||||
|
||||
private UUID id;
|
||||
|
||||
@NotBlank(message = "L'ID Stripe de l'abonnement est obligatoire")
|
||||
private String stripeSubscriptionId;
|
||||
|
||||
@NotBlank(message = "L'ID client Stripe est obligatoire")
|
||||
private String stripeCustomerId;
|
||||
|
||||
@NotBlank(message = "L'ID prix Stripe est obligatoire")
|
||||
private String stripePriceId;
|
||||
|
||||
@NotNull(message = "Le statut est obligatoire")
|
||||
private String status;
|
||||
|
||||
@NotNull(message = "Le début de période courante est obligatoire")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime currentPeriodStart;
|
||||
|
||||
@NotNull(message = "La fin de période courante est obligatoire")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime currentPeriodEnd;
|
||||
|
||||
private Boolean cancelAtPeriodEnd = false;
|
||||
|
||||
@NotNull(message = "Le cycle de facturation est obligatoire")
|
||||
private String billingCycle;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime nextBillingDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime trialEndDate;
|
||||
|
||||
// Relations
|
||||
private LicenseDto license;
|
||||
private List<InvoiceDto> invoices;
|
||||
|
||||
// Métadonnées
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
// ===== PROPRIÉTÉS CALCULÉES =====
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est actif
|
||||
*/
|
||||
public Boolean isActive() {
|
||||
return "ACTIVE".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est en période d'essai
|
||||
*/
|
||||
public Boolean isTrialing() {
|
||||
return "TRIALING".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est en retard de paiement
|
||||
*/
|
||||
public Boolean isPastDue() {
|
||||
return "PAST_DUE".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est annulé
|
||||
*/
|
||||
public Boolean isCanceled() {
|
||||
return "CANCELED".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours jusqu'à la prochaine facturation
|
||||
*/
|
||||
public Long getDaysUntilNextBilling() {
|
||||
if (nextBillingDate == null) return null;
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (now.isAfter(nextBillingDate)) return 0L;
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(now, nextBillingDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement nécessite une attention
|
||||
*/
|
||||
public Boolean requiresAttention() {
|
||||
return "PAST_DUE".equals(status) ||
|
||||
"UNPAID".equals(status) ||
|
||||
"INCOMPLETE".equals(status) ||
|
||||
"INCOMPLETE_EXPIRED".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours restants dans la période d'essai
|
||||
*/
|
||||
public Long getTrialDaysRemaining() {
|
||||
if (!"TRIALING".equals(status) || trialEndDate == null) return null;
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (now.isAfter(trialEndDate)) return 0L;
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(now, trialEndDate);
|
||||
}
|
||||
|
||||
// ===== SOUS-CLASSES POUR RESPONSES SPÉCIFIQUES =====
|
||||
|
||||
/**
|
||||
* Version minimale pour les listes
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Summary {
|
||||
private UUID id;
|
||||
private String stripeSubscriptionId;
|
||||
private String status;
|
||||
private String billingCycle;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime nextBillingDate;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private Boolean requiresAttention;
|
||||
private Long daysUntilNextBilling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version détaillée pour les vues individuelles
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Detailed extends SubscriptionDto {
|
||||
// Métriques additionnelles
|
||||
private Integer totalInvoices;
|
||||
private Integer unpaidInvoices;
|
||||
private String planName;
|
||||
private String companyName;
|
||||
|
||||
// Prochaines actions
|
||||
private String nextAction;
|
||||
private LocalDateTime nextActionDate;
|
||||
private String nextActionDescription;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package com.dh7789dev.xpeditis.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour les requêtes de création d'abonnement
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class CreateSubscriptionRequest {
|
||||
|
||||
@NotNull(message = "L'ID de l'entreprise est obligatoire")
|
||||
private UUID companyId;
|
||||
|
||||
@NotBlank(message = "L'ID du plan est obligatoire")
|
||||
private String planType;
|
||||
|
||||
@NotBlank(message = "Le cycle de facturation est obligatoire")
|
||||
private String billingCycle; // MONTHLY ou YEARLY
|
||||
|
||||
private UUID paymentMethodId;
|
||||
|
||||
// Options d'essai
|
||||
private Boolean startTrial = true;
|
||||
private Integer customTrialDays;
|
||||
|
||||
// Options de prorata
|
||||
private Boolean enableProration = true;
|
||||
|
||||
// Métadonnées personnalisées
|
||||
private String couponCode;
|
||||
private String referralSource;
|
||||
private String salesRepresentative;
|
||||
|
||||
// Validation
|
||||
public boolean isValidBillingCycle() {
|
||||
return "MONTHLY".equals(billingCycle) || "YEARLY".equals(billingCycle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Réponse pour la création d'abonnement
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
class CreateSubscriptionResponse {
|
||||
private UUID subscriptionId;
|
||||
private String stripeSubscriptionId;
|
||||
private String status;
|
||||
private String clientSecret; // Pour confirmer le paiement côté client si nécessaire
|
||||
private String nextAction; // Action requise (ex: authentification 3D Secure)
|
||||
private Boolean requiresPaymentMethod;
|
||||
private String hostedCheckoutUrl; // URL pour finaliser le paiement
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.dh7789dev.xpeditis.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO pour les requêtes de mise à jour d'abonnement
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class UpdateSubscriptionRequest {
|
||||
|
||||
// Changement de plan
|
||||
private String newPlanType;
|
||||
private String newBillingCycle;
|
||||
|
||||
// Changement de méthode de paiement
|
||||
private UUID newPaymentMethodId;
|
||||
|
||||
// Gestion de l'annulation
|
||||
private Boolean cancelAtPeriodEnd;
|
||||
private String cancellationReason;
|
||||
|
||||
// Options de prorata pour les changements
|
||||
private Boolean enableProration = true;
|
||||
private Boolean immediateChange = false;
|
||||
|
||||
// Validation
|
||||
public boolean isValidBillingCycle() {
|
||||
return newBillingCycle == null ||
|
||||
"MONTHLY".equals(newBillingCycle) ||
|
||||
"YEARLY".equals(newBillingCycle);
|
||||
}
|
||||
|
||||
public boolean hasChanges() {
|
||||
return newPlanType != null ||
|
||||
newBillingCycle != null ||
|
||||
newPaymentMethodId != null ||
|
||||
cancelAtPeriodEnd != null;
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
@ComponentScan(basePackages = {"com.dh7789dev.xpeditis"})
|
||||
@EnableJpaRepositories("com.dh7789dev.xpeditis.dao")
|
||||
@EnableJpaRepositories({"com.dh7789dev.xpeditis.dao", "com.dh7789dev.xpeditis.repository"})
|
||||
@EntityScan("com.dh7789dev.xpeditis.entity")
|
||||
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
|
||||
@SpringBootApplication
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.dh7789dev.xpeditis.configuration;
|
||||
|
||||
import com.dh7789dev.xpeditis.dao.UserDao;
|
||||
import com.dh7789dev.xpeditis.entity.UserEntity;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
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";
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
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,6 +11,7 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||
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.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.HeadersConfigurer;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
@ -27,7 +28,7 @@ import static org.springframework.security.web.util.matcher.AntPathRequestMatche
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
//@EnableMethodSecurity
|
||||
@EnableMethodSecurity(prePostEnabled = true)
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Value("${application.csrf.enabled}")
|
||||
@ -40,8 +41,18 @@ public class SecurityConfiguration {
|
||||
|
||||
private static final String[] WHITE_LIST_URL = {
|
||||
"/api/v1/auth/**",
|
||||
"/api/v1/devis/**",
|
||||
"/api/v1/grilles-tarifaires/**",
|
||||
"/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 = {
|
||||
"/v2/api-docs",
|
||||
"/v3/api-docs",
|
||||
@ -99,10 +110,9 @@ public class SecurityConfiguration {
|
||||
http.authorizeHttpRequests(auth ->
|
||||
auth.requestMatchers(WHITE_LIST_URL).permitAll()
|
||||
.requestMatchers(antMatcher(HttpMethod.GET, "/")).permitAll()
|
||||
.requestMatchers(antMatcher(HttpMethod.GET, API_V1_URI)).permitAll()
|
||||
.requestMatchers(antMatcher(HttpMethod.PUT, API_V1_URI)).hasRole(ADMIN_ROLE)
|
||||
.requestMatchers(antMatcher(HttpMethod.DELETE, API_V1_URI)).hasRole(ADMIN_ROLE)
|
||||
.requestMatchers(antMatcher(HttpMethod.POST, API_V1_URI)).hasRole(ADMIN_ROLE)
|
||||
.requestMatchers("/api/v1/users/**").authenticated()
|
||||
.requestMatchers("/api/v1/profile/**").authenticated()
|
||||
.requestMatchers(GENERAL_API_URL).authenticated()
|
||||
.requestMatchers(antMatcher("/h2-console/**")).access(INTERNAL_ACCESS)
|
||||
.requestMatchers(antMatcher("/actuator/**")).access(INTERNAL_ACCESS)
|
||||
.requestMatchers(INTERNAL_WHITE_LIST_URL).access(INTERNAL_ACCESS)
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
---
|
||||
spring:
|
||||
h2:
|
||||
console:
|
||||
enabled: 'false'
|
||||
enabled: ${SPRING_H2_CONSOLE_ENABLED:false}
|
||||
|
||||
datasource:
|
||||
url: jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
driverClassName: org.h2.Driver
|
||||
username: sa
|
||||
password: ''
|
||||
url: ${SPRING_H2_DATASOURCE_URL:jdbc:h2:mem:xpeditis;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE}
|
||||
driverClassName: ${SPRING_H2_DATASOURCE_DRIVER_CLASS_NAME:org.h2.Driver}
|
||||
username: ${SPRING_H2_DATASOURCE_USERNAME:sa}
|
||||
password: ${SPRING_H2_DATASOURCE_PASSWORD:}
|
||||
|
||||
sql:
|
||||
init:
|
||||
@ -16,45 +15,74 @@ spring:
|
||||
mode: always
|
||||
|
||||
jpa:
|
||||
show-sql: true
|
||||
show-sql: ${SPRING_JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
format_sql: ${SPRING_JPA_FORMAT_SQL:false}
|
||||
# show_sql: true
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
# database-platform: ${SPRING_JPA_DATABASE_PLATFORM_H2:org.hibernate.dialect.H2Dialect}
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
ddl-auto: ${SPRING_JPA_HIBERNATE_DDL_AUTO_DEV:create-drop}
|
||||
# Just to load initial data for the demo. DO NOT USE IT IN PRODUCTION
|
||||
defer-datasource-initialization: true
|
||||
defer-datasource-initialization: ${SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_DEV:true}
|
||||
|
||||
flyway: # flyway automatically uses the datasource from the application to connect to the DB
|
||||
enabled: false # enables flyway database migration
|
||||
enabled: ${SPRING_FLYWAY_ENABLED_DEV:false} # enables flyway database migration
|
||||
|
||||
mail:
|
||||
host: sandbox.smtp.mailtrap.io
|
||||
port: 2525
|
||||
username: 2597bd31d265eb
|
||||
password: cd126234193c89
|
||||
host: ${SPRING_MAIL_HOST_DEV:sandbox.smtp.mailtrap.io}
|
||||
port: ${SPRING_MAIL_PORT_DEV:2525}
|
||||
username: ${SPRING_MAIL_USERNAME_DEV:your_mailtrap_username}
|
||||
password: ${SPRING_MAIL_PASSWORD_DEV:your_mailtrap_password}
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
ssl:
|
||||
trust:"*"
|
||||
auth: true
|
||||
trust: ${SPRING_MAIL_SMTP_SSL_TRUST:*}
|
||||
auth: ${SPRING_MAIL_SMTP_AUTH:true}
|
||||
starttls:
|
||||
enable: true
|
||||
connectiontimeout: 5000
|
||||
timeout: 3000
|
||||
writetimeout: 5000
|
||||
enable: ${SPRING_MAIL_SMTP_STARTTLS_ENABLE:true}
|
||||
connectiontimeout: ${SPRING_MAIL_SMTP_CONNECTION_TIMEOUT:5000}
|
||||
timeout: ${SPRING_MAIL_SMTP_TIMEOUT:3000}
|
||||
writetimeout: ${SPRING_MAIL_SMTP_WRITE_TIMEOUT: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:
|
||||
email:
|
||||
from: randommailjf@gmail.com
|
||||
from: ${APPLICATION_EMAIL_FROM_DEV:noreply@xpeditis.local}
|
||||
csrf:
|
||||
enabled: false
|
||||
enabled: ${APPLICATION_CSRF_ENABLED_DEV:false}
|
||||
security:
|
||||
jwt:
|
||||
secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
|
||||
expiration: 86400000 # a day
|
||||
secret-key: ${JWT_SECRET_KEY:404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970}
|
||||
expiration: ${JWT_EXPIRATION:86400000} # a day
|
||||
refresh-token:
|
||||
expiration: 604800000 # 7 days
|
||||
expiration: ${JWT_REFRESH_TOKEN_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,10 +1,9 @@
|
||||
---
|
||||
spring:
|
||||
datasource:
|
||||
url: ${SPRING_DATASOURCE_URL}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
username: ${SPRING_DATASOURCE_USERNAME}
|
||||
password: ${SPRING_DATASOURCE_PASSWORD}
|
||||
url: ${SPRING_DATASOURCE_URL:}
|
||||
driver-class-name: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver}
|
||||
username: ${SPRING_DATASOURCE_USERNAME:}
|
||||
password: ${SPRING_DATASOURCE_PASSWORD:}
|
||||
#hikari:
|
||||
#schema: leblr
|
||||
|
||||
@ -15,52 +14,83 @@ spring:
|
||||
#data-locations: import_users.sql
|
||||
|
||||
jpa:
|
||||
# show-sql: true
|
||||
show-sql: ${SPRING_JPA_SHOW_SQL:false}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
format_sql: ${SPRING_JPA_FORMAT_SQL:true}
|
||||
#show_sql: true
|
||||
database: mysql
|
||||
database-platform: org.hibernate.dialect.MySQLDialect
|
||||
database-platform: ${SPRING_JPA_DATABASE_PLATFORM_MYSQL:org.hibernate.dialect.MySQLDialect}
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
defer-datasource-initialization: false
|
||||
ddl-auto: ${SPRING_JPA_HIBERNATE_DDL_AUTO_PROD:validate}
|
||||
defer-datasource-initialization: ${SPRING_JPA_DEFER_DATASOURCE_INITIALIZATION_PROD:false}
|
||||
#open-in-view: false
|
||||
|
||||
flyway: # flyway automatically uses the datasource from the application to connect to the DB
|
||||
enabled: true # enables flyway database migration
|
||||
locations: classpath:db/migration/structure, classpath:db/migration/data # the location where flyway should look for migration scripts
|
||||
validate-on-migrate: true
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
default-schema: leblr
|
||||
enabled: ${SPRING_FLYWAY_ENABLED_PROD: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
|
||||
validate-on-migrate: ${SPRING_FLYWAY_VALIDATE_ON_MIGRATE:true}
|
||||
baseline-on-migrate: ${SPRING_FLYWAY_BASELINE_ON_MIGRATE:true}
|
||||
baseline-version: ${SPRING_FLYWAY_BASELINE_VERSION:0}
|
||||
default-schema: ${SPRING_FLYWAY_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:
|
||||
protocol: smtp
|
||||
host: ssl0.ovh.net
|
||||
port: 587
|
||||
username: contact@xpeditis.fr
|
||||
password:
|
||||
protocol: ${SPRING_MAIL_PROTOCOL_PROD:smtp}
|
||||
host: ${SPRING_MAIL_HOST_PROD:ssl0.ovh.net}
|
||||
port: ${SPRING_MAIL_PORT_PROD:587}
|
||||
username: ${SPRING_MAIL_USERNAME_PROD:contact@xpeditis.fr}
|
||||
password: ${SPRING_MAIL_PASSWORD_PROD:}
|
||||
properties:
|
||||
mail:
|
||||
smtp:
|
||||
auth: true
|
||||
auth: ${SPRING_MAIL_SMTP_AUTH:true}
|
||||
starttls:
|
||||
enable: true
|
||||
connectiontimeout: 5000
|
||||
timeout: 3000
|
||||
writetimeout: 5000
|
||||
enable: ${SPRING_MAIL_SMTP_STARTTLS_ENABLE:true}
|
||||
connectiontimeout: ${SPRING_MAIL_SMTP_CONNECTION_TIMEOUT:5000}
|
||||
timeout: ${SPRING_MAIL_SMTP_TIMEOUT:3000}
|
||||
writetimeout: ${SPRING_MAIL_SMTP_WRITE_TIMEOUT:5000}
|
||||
|
||||
application:
|
||||
email:
|
||||
from: contact@leblr.fr
|
||||
from: ${APPLICATION_EMAIL_FROM_PROD:contact@xpeditis.fr}
|
||||
|
||||
csrf:
|
||||
enabled: false
|
||||
enabled: ${APPLICATION_CSRF_ENABLED_PROD:true}
|
||||
|
||||
security:
|
||||
jwt:
|
||||
secret-key: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
|
||||
expiration: 86400000 # a day
|
||||
secret-key: ${JWT_SECRET_KEY}
|
||||
expiration: ${JWT_EXPIRATION:86400000} # a day
|
||||
refresh-token:
|
||||
expiration: 604800000 # 7 days
|
||||
expiration: ${JWT_REFRESH_TOKEN_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:
|
||||
port: 8080
|
||||
port: ${SERVER_PORT:8080}
|
||||
|
||||
file:
|
||||
upload-dir: /upload
|
||||
upload-dir: ${FILE_UPLOAD_DIR:/upload}
|
||||
|
||||
spring:
|
||||
http:
|
||||
@ -12,11 +12,11 @@ spring:
|
||||
force: true
|
||||
|
||||
application:
|
||||
name: '@project.description@'
|
||||
version: '@project.version@'
|
||||
name: ${SPRING_APPLICATION_NAME:@project.description@}
|
||||
version: ${SPRING_APPLICATION_VERSION:@project.version@}
|
||||
|
||||
profiles:
|
||||
active: '@spring.profiles.active@'
|
||||
active: ${SPRING_PROFILES_ACTIVE:@spring.profiles.active@}
|
||||
|
||||
banner:
|
||||
location: 'classpath:banner.txt'
|
||||
@ -30,21 +30,22 @@ spring:
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
enabled: true
|
||||
max-file-size: 50MB
|
||||
max-request-size: 50MB
|
||||
enabled: ${SPRING_SERVLET_MULTIPART_ENABLED:true}
|
||||
max-file-size: ${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:50MB}
|
||||
max-request-size: ${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:50MB}
|
||||
#location: ${java.io.tmpdir}
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: ${LOGGING_LEVEL_ROOT:INFO}
|
||||
org:
|
||||
org.hibernate.orm.query.sqm.ast.logTree: OFF
|
||||
org.hibernate.orm.query.sqm.ast.logTree: ${LOGGING_LEVEL_HIBERNATE_SQL:OFF}
|
||||
springframework:
|
||||
boot:
|
||||
autoconfigure: OFF
|
||||
autoconfigure: ${LOGGING_LEVEL_SPRINGFRAMEWORK_BOOT_AUTOCONFIGURE:OFF}
|
||||
web:
|
||||
filter:
|
||||
CommonsRequestLoggingFilter: INFO
|
||||
CommonsRequestLoggingFilter: ${LOGGING_LEVEL_COMMONS_REQUEST_LOGGING_FILTER:INFO}
|
||||
security:
|
||||
config:
|
||||
annotation:
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
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,12 +1,24 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.UserAccount;
|
||||
import com.dh7789dev.xpeditis.dto.request.AuthenticationRequest;
|
||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||
import com.dh7789dev.xpeditis.dto.request.RegisterRequest;
|
||||
import com.dh7789dev.xpeditis.dto.response.AuthenticationResponse;
|
||||
import com.dh7789dev.xpeditis.port.in.AuthenticationUseCase;
|
||||
|
||||
public interface AuthenticationService {
|
||||
public interface AuthenticationService extends AuthenticationUseCase {
|
||||
|
||||
AuthenticationResponse authenticate(AuthenticationRequest 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,4 +1,24 @@
|
||||
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 {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
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);
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
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,4 +1,25 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
public interface LicenseService {
|
||||
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.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);
|
||||
}
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||
import com.dh7789dev.xpeditis.dto.app.BillingCycle;
|
||||
import com.dh7789dev.xpeditis.dto.valueobject.Money;
|
||||
import com.dh7789dev.xpeditis.port.in.PlanManagementUseCase;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service de gestion des plans d'abonnement
|
||||
*/
|
||||
public interface PlanService extends PlanManagementUseCase {
|
||||
|
||||
/**
|
||||
* Récupère tous les plans actifs
|
||||
*/
|
||||
List<Plan> getAllActivePlans();
|
||||
|
||||
/**
|
||||
* Récupère un plan par son ID
|
||||
*/
|
||||
Plan getPlanById(UUID planId);
|
||||
|
||||
/**
|
||||
* Récupère un plan par son type
|
||||
*/
|
||||
Plan getPlanByType(LicenseType type);
|
||||
|
||||
/**
|
||||
* Récupère un plan par son ID Stripe
|
||||
*/
|
||||
Plan getPlanByStripePriceId(String stripePriceId);
|
||||
|
||||
/**
|
||||
* Calcule le montant prorata pour un changement de plan
|
||||
*/
|
||||
Money calculateProrata(Plan currentPlan, Plan newPlan, BillingCycle cycle, int daysRemaining);
|
||||
|
||||
/**
|
||||
* Trouve les plans adaptés à un nombre d'utilisateurs
|
||||
*/
|
||||
List<Plan> findSuitablePlansForUserCount(int userCount);
|
||||
|
||||
/**
|
||||
* Compare deux plans et retourne les différences
|
||||
*/
|
||||
PlanComparison comparePlans(Plan plan1, Plan plan2);
|
||||
|
||||
/**
|
||||
* Vérifie si un upgrade/downgrade est possible
|
||||
*/
|
||||
boolean canChangePlan(Plan currentPlan, Plan targetPlan, int currentUserCount);
|
||||
|
||||
/**
|
||||
* Récupère le plan recommandé pour une entreprise
|
||||
*/
|
||||
Plan getRecommendedPlan(int userCount, List<String> requiredFeatures);
|
||||
|
||||
/**
|
||||
* Classe interne pour la comparaison de plans
|
||||
*/
|
||||
class PlanComparison {
|
||||
private final Plan plan1;
|
||||
private final Plan plan2;
|
||||
private final List<String> addedFeatures;
|
||||
private final List<String> removedFeatures;
|
||||
private final Money priceDifference;
|
||||
private final boolean isUpgrade;
|
||||
|
||||
public PlanComparison(Plan plan1, Plan plan2, List<String> addedFeatures,
|
||||
List<String> removedFeatures, Money priceDifference, boolean isUpgrade) {
|
||||
this.plan1 = plan1;
|
||||
this.plan2 = plan2;
|
||||
this.addedFeatures = addedFeatures;
|
||||
this.removedFeatures = removedFeatures;
|
||||
this.priceDifference = priceDifference;
|
||||
this.isUpgrade = isUpgrade;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public Plan getPlan1() { return plan1; }
|
||||
public Plan getPlan2() { return plan2; }
|
||||
public List<String> getAddedFeatures() { return addedFeatures; }
|
||||
public List<String> getRemovedFeatures() { return removedFeatures; }
|
||||
public Money getPriceDifference() { return priceDifference; }
|
||||
public boolean isUpgrade() { return isUpgrade; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Subscription;
|
||||
import com.dh7789dev.xpeditis.dto.app.SubscriptionStatus;
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import com.dh7789dev.xpeditis.dto.app.BillingCycle;
|
||||
import com.dh7789dev.xpeditis.port.in.SubscriptionManagementUseCase;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service de gestion des abonnements avec intégration Stripe
|
||||
*/
|
||||
public interface SubscriptionService extends SubscriptionManagementUseCase {
|
||||
|
||||
/**
|
||||
* Crée un abonnement Stripe et la licence associée
|
||||
*/
|
||||
Subscription createSubscription(UUID companyId, Plan plan, BillingCycle billingCycle);
|
||||
|
||||
/**
|
||||
* Met à jour un abonnement depuis un webhook Stripe
|
||||
*/
|
||||
Subscription updateSubscriptionFromStripe(String stripeSubscriptionId, SubscriptionStatus newStatus);
|
||||
|
||||
/**
|
||||
* Change le plan d'un abonnement
|
||||
*/
|
||||
Subscription changePlan(UUID companyId, Plan newPlan, boolean immediate);
|
||||
|
||||
/**
|
||||
* Annule un abonnement
|
||||
*/
|
||||
Subscription cancelSubscription(UUID companyId, boolean immediate, String reason);
|
||||
|
||||
/**
|
||||
* Réactive un abonnement annulé
|
||||
*/
|
||||
Subscription reactivateSubscription(UUID companyId);
|
||||
|
||||
/**
|
||||
* Récupère l'abonnement actif d'une entreprise
|
||||
*/
|
||||
Subscription getActiveSubscription(UUID companyId);
|
||||
|
||||
/**
|
||||
* Traite un échec de paiement
|
||||
*/
|
||||
void handlePaymentFailure(String stripeSubscriptionId, String reason);
|
||||
|
||||
/**
|
||||
* Traite un paiement réussi
|
||||
*/
|
||||
void handlePaymentSuccess(String stripeSubscriptionId, String stripeInvoiceId);
|
||||
|
||||
/**
|
||||
* Récupère les abonnements nécessitant une attention
|
||||
*/
|
||||
List<Subscription> getSubscriptionsRequiringAttention();
|
||||
|
||||
/**
|
||||
* Démarre la période de grâce pour un abonnement
|
||||
*/
|
||||
void startGracePeriod(UUID subscriptionId);
|
||||
|
||||
/**
|
||||
* Suspend les abonnements impayés
|
||||
*/
|
||||
void suspendUnpaidSubscriptions();
|
||||
}
|
||||
@ -1,10 +1,35 @@
|
||||
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.port.in.UserManagementUseCase;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface UserService {
|
||||
public interface UserService extends UserManagementUseCase {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
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);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
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);
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.dh7789dev.xpeditis.port.in;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import com.dh7789dev.xpeditis.dto.app.LicenseType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Port d'entrée pour la gestion des plans d'abonnement
|
||||
*/
|
||||
public interface PlanManagementUseCase {
|
||||
|
||||
/**
|
||||
* Récupère tous les plans disponibles
|
||||
*/
|
||||
List<Plan> getAllActivePlans();
|
||||
|
||||
/**
|
||||
* Récupère un plan par son ID
|
||||
*/
|
||||
Plan getPlanById(UUID planId);
|
||||
|
||||
/**
|
||||
* Récupère un plan par son type de licence
|
||||
*/
|
||||
Plan getPlanByType(LicenseType type);
|
||||
|
||||
/**
|
||||
* Trouve les plans adaptés à un nombre d'utilisateurs
|
||||
*/
|
||||
List<Plan> findSuitablePlansForUserCount(int userCount);
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.dh7789dev.xpeditis.port.in;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.Subscription;
|
||||
import com.dh7789dev.xpeditis.dto.app.Plan;
|
||||
import com.dh7789dev.xpeditis.dto.app.BillingCycle;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Port d'entrée pour la gestion des abonnements
|
||||
*/
|
||||
public interface SubscriptionManagementUseCase {
|
||||
|
||||
/**
|
||||
* Crée un abonnement avec un plan et cycle de facturation
|
||||
*/
|
||||
Subscription createSubscription(UUID companyId, Plan plan, BillingCycle billingCycle);
|
||||
|
||||
/**
|
||||
* Change le plan d'un abonnement existant
|
||||
*/
|
||||
Subscription changePlan(UUID companyId, Plan newPlan, boolean immediate);
|
||||
|
||||
/**
|
||||
* Annule un abonnement
|
||||
*/
|
||||
Subscription cancelSubscription(UUID companyId, boolean immediate, String reason);
|
||||
|
||||
/**
|
||||
* Récupère l'abonnement actif d'une entreprise
|
||||
*/
|
||||
Subscription getActiveSubscription(UUID companyId);
|
||||
|
||||
/**
|
||||
* Réactive un abonnement suspendu ou annulé
|
||||
*/
|
||||
Subscription reactivateSubscription(UUID companyId);
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
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);
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
public enum AuthProvider {
|
||||
LOCAL,
|
||||
GOOGLE
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
/**
|
||||
* Cycles de facturation disponibles
|
||||
*/
|
||||
public enum BillingCycle {
|
||||
/**
|
||||
* Facturation mensuelle
|
||||
*/
|
||||
MONTHLY("month", 1),
|
||||
|
||||
/**
|
||||
* Facturation annuelle
|
||||
*/
|
||||
YEARLY("year", 12);
|
||||
|
||||
private final String stripeInterval;
|
||||
private final int months;
|
||||
|
||||
BillingCycle(String stripeInterval, int months) {
|
||||
this.stripeInterval = stripeInterval;
|
||||
this.months = months;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'intervalle Stripe correspondant
|
||||
*/
|
||||
public String getStripeInterval() {
|
||||
return stripeInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le nombre de mois pour ce cycle
|
||||
*/
|
||||
public int getMonths() {
|
||||
return months;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un cycle mensuel
|
||||
*/
|
||||
public boolean isMonthly() {
|
||||
return this == MONTHLY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un cycle annuel
|
||||
*/
|
||||
public boolean isYearly() {
|
||||
return this == YEARLY;
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,50 @@
|
||||
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.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Company {
|
||||
private Long id;
|
||||
private UUID id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String website;
|
||||
private String industry;
|
||||
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<License> licenses;
|
||||
private License license; // Current active license
|
||||
private List<Quote> quotes;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
/**
|
||||
* Statuts de traitement des événements webhook
|
||||
*/
|
||||
public enum EventStatus {
|
||||
/**
|
||||
* Événement reçu mais pas encore traité
|
||||
*/
|
||||
PENDING,
|
||||
|
||||
/**
|
||||
* Événement traité avec succès
|
||||
*/
|
||||
PROCESSED,
|
||||
|
||||
/**
|
||||
* Échec du traitement de l'événement
|
||||
*/
|
||||
FAILED,
|
||||
|
||||
/**
|
||||
* Événement en cours de traitement
|
||||
*/
|
||||
PROCESSING,
|
||||
|
||||
/**
|
||||
* Événement ignoré (déjà traité ou non pertinent)
|
||||
*/
|
||||
IGNORED
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Facture générée pour un abonnement
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Invoice {
|
||||
private UUID id;
|
||||
private String stripeInvoiceId;
|
||||
private String invoiceNumber;
|
||||
private Subscription subscription;
|
||||
private InvoiceStatus status;
|
||||
private BigDecimal amountDue;
|
||||
private BigDecimal amountPaid;
|
||||
private String currency;
|
||||
private LocalDateTime billingPeriodStart;
|
||||
private LocalDateTime billingPeriodEnd;
|
||||
private LocalDateTime dueDate;
|
||||
private LocalDateTime paidAt;
|
||||
private String invoicePdfUrl;
|
||||
private String hostedInvoiceUrl;
|
||||
private Integer attemptCount;
|
||||
private List<InvoiceLineItem> lineItems;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* @return true si la facture est payée
|
||||
*/
|
||||
public boolean isPaid() {
|
||||
return status == InvoiceStatus.PAID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est en attente de paiement
|
||||
*/
|
||||
public boolean isOpen() {
|
||||
return status == InvoiceStatus.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est en retard
|
||||
*/
|
||||
public boolean isOverdue() {
|
||||
return isOpen() && dueDate != null && LocalDateTime.now().isAfter(dueDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est annulée
|
||||
*/
|
||||
public boolean isVoided() {
|
||||
return status == InvoiceStatus.VOID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le montant restant à payer
|
||||
*/
|
||||
public BigDecimal getAmountRemaining() {
|
||||
if (amountDue == null) return BigDecimal.ZERO;
|
||||
if (amountPaid == null) return amountDue;
|
||||
return amountDue.subtract(amountPaid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la facture est entièrement payée
|
||||
*/
|
||||
public boolean isFullyPaid() {
|
||||
return getAmountRemaining().compareTo(BigDecimal.ZERO) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours depuis la date d'échéance
|
||||
*/
|
||||
public long getDaysOverdue() {
|
||||
if (dueDate == null || !isOverdue()) return 0;
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(dueDate, LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours jusqu'à l'échéance (négatif si en retard)
|
||||
*/
|
||||
public long getDaysUntilDue() {
|
||||
if (dueDate == null) return Long.MAX_VALUE;
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(LocalDateTime.now(), dueDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return la période de facturation au format texte
|
||||
*/
|
||||
public String getBillingPeriodDescription() {
|
||||
if (billingPeriodStart == null || billingPeriodEnd == null) {
|
||||
return "Période inconnue";
|
||||
}
|
||||
|
||||
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("dd/MM/yyyy");
|
||||
return "Du " + billingPeriodStart.format(formatter) + " au " + billingPeriodEnd.format(formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrémente le compteur de tentatives de paiement
|
||||
*/
|
||||
public void incrementAttemptCount() {
|
||||
this.attemptCount = (attemptCount == null ? 0 : attemptCount) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque la facture comme payée
|
||||
*/
|
||||
public void markAsPaid(LocalDateTime paidAt, BigDecimal amountPaid) {
|
||||
this.status = InvoiceStatus.PAID;
|
||||
this.paidAt = paidAt;
|
||||
this.amountPaid = amountPaid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette facture nécessite une attention immédiate
|
||||
*/
|
||||
public boolean requiresAttention() {
|
||||
return isOverdue() || (isOpen() && getDaysUntilDue() <= 3);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Ligne d'une facture (détail d'un service facturé)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InvoiceLineItem {
|
||||
private UUID id;
|
||||
private Invoice invoice;
|
||||
private String description;
|
||||
private Integer quantity;
|
||||
private BigDecimal unitPrice;
|
||||
private BigDecimal amount;
|
||||
private String stripePriceId;
|
||||
private LocalDateTime periodStart;
|
||||
private LocalDateTime periodEnd;
|
||||
private boolean prorated;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* @return le montant total de cette ligne (quantity * unitPrice)
|
||||
*/
|
||||
public BigDecimal getTotalAmount() {
|
||||
if (amount != null) {
|
||||
return amount;
|
||||
}
|
||||
if (quantity != null && unitPrice != null) {
|
||||
return unitPrice.multiply(BigDecimal.valueOf(quantity));
|
||||
}
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette ligne concerne une période de service
|
||||
*/
|
||||
public boolean isPeriodBased() {
|
||||
return periodStart != null && periodEnd != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return la description de la période de service
|
||||
*/
|
||||
public String getPeriodDescription() {
|
||||
if (!isPeriodBased()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("dd/MM/yyyy");
|
||||
return "Du " + periodStart.format(formatter) + " au " + periodEnd.format(formatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un ajustement prorata
|
||||
*/
|
||||
public boolean isProrated() {
|
||||
return prorated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return une description complète de la ligne
|
||||
*/
|
||||
public String getFullDescription() {
|
||||
StringBuilder desc = new StringBuilder();
|
||||
|
||||
if (description != null) {
|
||||
desc.append(description);
|
||||
}
|
||||
|
||||
if (isPeriodBased()) {
|
||||
if (desc.length() > 0) {
|
||||
desc.append(" - ");
|
||||
}
|
||||
desc.append(getPeriodDescription());
|
||||
}
|
||||
|
||||
if (isProrated()) {
|
||||
if (desc.length() > 0) {
|
||||
desc.append(" ");
|
||||
}
|
||||
desc.append("(prorata)");
|
||||
}
|
||||
|
||||
return desc.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
/**
|
||||
* Statuts possibles d'une facture
|
||||
*/
|
||||
public enum InvoiceStatus {
|
||||
/**
|
||||
* Facture en brouillon (non finalisée)
|
||||
*/
|
||||
DRAFT,
|
||||
|
||||
/**
|
||||
* Facture ouverte en attente de paiement
|
||||
*/
|
||||
OPEN,
|
||||
|
||||
/**
|
||||
* Facture payée avec succès
|
||||
*/
|
||||
PAID,
|
||||
|
||||
/**
|
||||
* Facture annulée
|
||||
*/
|
||||
VOID,
|
||||
|
||||
/**
|
||||
* Facture irrécupérable (après plusieurs tentatives d'échec)
|
||||
*/
|
||||
UNCOLLECTIBLE,
|
||||
|
||||
/**
|
||||
* Facture marquée comme non collectible par Stripe
|
||||
*/
|
||||
MARKED_UNCOLLECTIBLE
|
||||
}
|
||||
@ -1,16 +1,201 @@
|
||||
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;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class License {
|
||||
private Long id;
|
||||
private UUID id;
|
||||
private String licenseKey;
|
||||
private LicenseType type;
|
||||
private LicenseStatus status;
|
||||
private LocalDate startDate;
|
||||
private LocalDate expirationDate;
|
||||
private boolean active;
|
||||
private UserAccount user;
|
||||
private LocalDateTime issuedDate;
|
||||
private LocalDateTime expiryDate;
|
||||
private LocalDateTime gracePeriodEndDate;
|
||||
private int maxUsers;
|
||||
private Set<String> featuresEnabled;
|
||||
private boolean isActive;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime lastCheckedAt;
|
||||
private Company company;
|
||||
private Subscription subscription;
|
||||
|
||||
/**
|
||||
* @return true si la licence est expirée (date d'expiration dépassée)
|
||||
*/
|
||||
public boolean isExpired() {
|
||||
return expirationDate != null && expirationDate.isBefore(LocalDate.now());
|
||||
}
|
||||
|
||||
public LocalDateTime getExpiryDate() {
|
||||
return expiryDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence est active (pas suspendue, pas expirée)
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return status == LicenseStatus.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence est valide (active ou en période de grâce)
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return isActive() || isInGracePeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence est en période de grâce
|
||||
*/
|
||||
public boolean isInGracePeriod() {
|
||||
return status == LicenseStatus.GRACE_PERIOD
|
||||
&& gracePeriodEndDate != null
|
||||
&& LocalDateTime.now().isBefore(gracePeriodEndDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence est suspendue
|
||||
*/
|
||||
public boolean isSuspended() {
|
||||
return status == LicenseStatus.SUSPENDED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence est annulée
|
||||
*/
|
||||
public boolean isCancelled() {
|
||||
return status == LicenseStatus.CANCELLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si un utilisateur peut être ajouté
|
||||
*/
|
||||
public boolean canAddUser(int currentUserCount) {
|
||||
return isValid() && (!hasUserLimit() || currentUserCount < maxUsers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si ce type de licence a une limite d'utilisateurs
|
||||
*/
|
||||
public boolean hasUserLimit() {
|
||||
return type != null && type.hasUserLimit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours jusqu'à l'expiration
|
||||
*/
|
||||
public long getDaysUntilExpiration() {
|
||||
return expirationDate != null ?
|
||||
java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(), expirationDate) :
|
||||
Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours restants en période de grâce (0 si pas en période de grâce)
|
||||
*/
|
||||
public long getDaysRemainingInGracePeriod() {
|
||||
if (!isInGracePeriod() || gracePeriodEndDate == null) {
|
||||
return 0;
|
||||
}
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(LocalDateTime.now(), gracePeriodEndDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la fonctionnalité est activée pour cette licence
|
||||
*/
|
||||
public boolean hasFeature(String featureCode) {
|
||||
return featuresEnabled != null && featuresEnabled.contains(featureCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Active une fonctionnalité
|
||||
*/
|
||||
public void enableFeature(String featureCode) {
|
||||
if (featuresEnabled == null) {
|
||||
featuresEnabled = new java.util.HashSet<>();
|
||||
}
|
||||
featuresEnabled.add(featureCode);
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Désactive une fonctionnalité
|
||||
*/
|
||||
public void disableFeature(String featureCode) {
|
||||
if (featuresEnabled != null) {
|
||||
featuresEnabled.remove(featureCode);
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut de la licence
|
||||
*/
|
||||
public void updateStatus(LicenseStatus newStatus) {
|
||||
this.status = newStatus;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
|
||||
// Si on sort de la période de grâce, on reset la date
|
||||
if (newStatus != LicenseStatus.GRACE_PERIOD) {
|
||||
this.gracePeriodEndDate = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarre la période de grâce
|
||||
*/
|
||||
public void startGracePeriod(int gracePeriodDays) {
|
||||
this.status = LicenseStatus.GRACE_PERIOD;
|
||||
this.gracePeriodEndDate = LocalDateTime.now().plusDays(gracePeriodDays);
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend la licence
|
||||
*/
|
||||
public void suspend() {
|
||||
this.status = LicenseStatus.SUSPENDED;
|
||||
this.isActive = false;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Réactive la licence
|
||||
*/
|
||||
public void reactivate() {
|
||||
this.status = LicenseStatus.ACTIVE;
|
||||
this.isActive = true;
|
||||
this.gracePeriodEndDate = null;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la licence nécessite une attention immédiate
|
||||
*/
|
||||
public boolean requiresAttention() {
|
||||
return isSuspended()
|
||||
|| (isInGracePeriod() && getDaysRemainingInGracePeriod() <= 1)
|
||||
|| (getDaysUntilExpiration() <= 7 && getDaysUntilExpiration() > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la dernière vérification
|
||||
*/
|
||||
public void updateLastChecked() {
|
||||
this.lastCheckedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
/**
|
||||
* Statuts possibles d'une licence
|
||||
*/
|
||||
public enum LicenseStatus {
|
||||
/**
|
||||
* Licence active et valide
|
||||
*/
|
||||
ACTIVE,
|
||||
|
||||
/**
|
||||
* Licence expirée mais dans la période de grâce
|
||||
*/
|
||||
GRACE_PERIOD,
|
||||
|
||||
/**
|
||||
* Licence suspendue pour non-paiement
|
||||
*/
|
||||
SUSPENDED,
|
||||
|
||||
/**
|
||||
* Licence expirée définitivement
|
||||
*/
|
||||
EXPIRED,
|
||||
|
||||
/**
|
||||
* Licence annulée par l'utilisateur
|
||||
*/
|
||||
CANCELLED,
|
||||
|
||||
/**
|
||||
* Licence révoquée par le système
|
||||
*/
|
||||
REVOKED
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public enum LicenseType {
|
||||
TRIAL(5, 30, "Trial", BigDecimal.ZERO, BigDecimal.ZERO),
|
||||
BASIC(10, -1, "Basic Plan", BigDecimal.valueOf(29.00), BigDecimal.valueOf(278.00)),
|
||||
PREMIUM(50, -1, "Premium Plan", BigDecimal.valueOf(79.00), BigDecimal.valueOf(758.00)),
|
||||
ENTERPRISE(-1, -1, "Enterprise Plan", BigDecimal.valueOf(199.00), BigDecimal.valueOf(1908.00));
|
||||
|
||||
private final int maxUsers;
|
||||
private final int durationDays;
|
||||
private final String description;
|
||||
private final BigDecimal basePrice;
|
||||
private final BigDecimal yearlyPrice;
|
||||
|
||||
LicenseType(int maxUsers, int durationDays, String description, BigDecimal basePrice, BigDecimal yearlyPrice) {
|
||||
this.maxUsers = maxUsers;
|
||||
this.durationDays = durationDays;
|
||||
this.description = description;
|
||||
this.basePrice = basePrice;
|
||||
this.yearlyPrice = yearlyPrice;
|
||||
}
|
||||
|
||||
public int getMaxUsers() {
|
||||
return maxUsers;
|
||||
}
|
||||
|
||||
public int getDurationDays() {
|
||||
return durationDays;
|
||||
}
|
||||
|
||||
public boolean hasUserLimit() {
|
||||
return maxUsers > 0;
|
||||
}
|
||||
|
||||
public boolean hasTimeLimit() {
|
||||
return durationDays > 0;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public BigDecimal getBasePrice() {
|
||||
return basePrice;
|
||||
}
|
||||
|
||||
public BigDecimal getYearlyPrice() {
|
||||
return yearlyPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un plan gratuit
|
||||
*/
|
||||
public boolean isFree() {
|
||||
return this == TRIAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un plan payant
|
||||
*/
|
||||
public boolean isPaid() {
|
||||
return !isFree();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le pourcentage d'économies annuelles
|
||||
*/
|
||||
public BigDecimal getYearlySavingsPercentage() {
|
||||
if (basePrice.compareTo(BigDecimal.ZERO) == 0 || yearlyPrice.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
BigDecimal yearlyEquivalent = basePrice.multiply(BigDecimal.valueOf(12));
|
||||
BigDecimal savings = yearlyEquivalent.subtract(yearlyPrice);
|
||||
return savings.divide(yearlyEquivalent, 4, java.math.RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,161 @@
|
||||
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;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Événement de paiement reçu via webhook Stripe
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentEvent {
|
||||
private UUID id;
|
||||
private String stripeEventId;
|
||||
private String eventType;
|
||||
private EventStatus status;
|
||||
private Map<String, Object> payload;
|
||||
private LocalDateTime processedAt;
|
||||
private String errorMessage;
|
||||
private Integer retryCount;
|
||||
private Subscription subscription;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* @return true si l'événement a été traité avec succès
|
||||
*/
|
||||
public boolean isProcessed() {
|
||||
return status == EventStatus.PROCESSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'événement a échoué
|
||||
*/
|
||||
public boolean isFailed() {
|
||||
return status == EventStatus.FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'événement est en attente de traitement
|
||||
*/
|
||||
public boolean isPending() {
|
||||
return status == EventStatus.PENDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'événement est en cours de traitement
|
||||
*/
|
||||
public boolean isProcessing() {
|
||||
return status == EventStatus.PROCESSING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'événement peut être réessayé
|
||||
*/
|
||||
public boolean canRetry() {
|
||||
return isFailed() && (retryCount == null || retryCount < 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrémente le compteur de tentatives
|
||||
*/
|
||||
public void incrementRetryCount() {
|
||||
this.retryCount = (retryCount == null ? 0 : retryCount) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque l'événement comme traité avec succès
|
||||
*/
|
||||
public void markAsProcessed() {
|
||||
this.status = EventStatus.PROCESSED;
|
||||
this.processedAt = LocalDateTime.now();
|
||||
this.errorMessage = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque l'événement comme échoué
|
||||
*/
|
||||
public void markAsFailed(String errorMessage) {
|
||||
this.status = EventStatus.FAILED;
|
||||
this.errorMessage = errorMessage;
|
||||
incrementRetryCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque l'événement comme en cours de traitement
|
||||
*/
|
||||
public void markAsProcessing() {
|
||||
this.status = EventStatus.PROCESSING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque l'événement comme ignoré
|
||||
*/
|
||||
public void markAsIgnored() {
|
||||
this.status = EventStatus.IGNORED;
|
||||
this.processedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un événement concernant une facture
|
||||
*/
|
||||
public boolean isInvoiceEvent() {
|
||||
return eventType != null && eventType.startsWith("invoice.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un événement concernant un abonnement
|
||||
*/
|
||||
public boolean isSubscriptionEvent() {
|
||||
return eventType != null && eventType.startsWith("customer.subscription.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un événement concernant un paiement
|
||||
*/
|
||||
public boolean isPaymentEvent() {
|
||||
return eventType != null && (
|
||||
eventType.startsWith("payment_intent.") ||
|
||||
eventType.startsWith("invoice.payment_")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return une description lisible de l'événement
|
||||
*/
|
||||
public String getEventDescription() {
|
||||
if (eventType == null) return "Événement inconnu";
|
||||
|
||||
switch (eventType) {
|
||||
case "customer.subscription.created":
|
||||
return "Abonnement créé";
|
||||
case "customer.subscription.updated":
|
||||
return "Abonnement modifié";
|
||||
case "customer.subscription.deleted":
|
||||
return "Abonnement supprimé";
|
||||
case "invoice.payment_succeeded":
|
||||
return "Paiement réussi";
|
||||
case "invoice.payment_failed":
|
||||
return "Paiement échoué";
|
||||
case "invoice.created":
|
||||
return "Facture créée";
|
||||
case "invoice.finalized":
|
||||
return "Facture finalisée";
|
||||
case "checkout.session.completed":
|
||||
return "Session de paiement complétée";
|
||||
case "payment_method.attached":
|
||||
return "Méthode de paiement ajoutée";
|
||||
case "payment_method.detached":
|
||||
return "Méthode de paiement supprimée";
|
||||
default:
|
||||
return eventType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,145 @@
|
||||
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.UUID;
|
||||
|
||||
/**
|
||||
* Méthode de paiement d'un client
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentMethod {
|
||||
private UUID id;
|
||||
private String stripePaymentMethodId;
|
||||
private PaymentType type;
|
||||
private boolean isDefault;
|
||||
private String cardBrand;
|
||||
private String cardLast4;
|
||||
private Integer cardExpMonth;
|
||||
private Integer cardExpYear;
|
||||
private String bankName;
|
||||
private Company company;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* @return true si c'est une carte bancaire
|
||||
*/
|
||||
public boolean isCard() {
|
||||
return type == PaymentType.CARD;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un prélèvement SEPA
|
||||
*/
|
||||
public boolean isSepaDebit() {
|
||||
return type == PaymentType.SEPA_DEBIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si c'est un virement bancaire
|
||||
*/
|
||||
public boolean isBankTransfer() {
|
||||
return type == PaymentType.BANK_TRANSFER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la carte expire bientôt (dans les 30 jours)
|
||||
*/
|
||||
public boolean isCardExpiringSoon() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime cardExpiry = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1).minusDays(1); // Dernier jour du mois d'expiration
|
||||
|
||||
return cardExpiry.isBefore(now.plusDays(30));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la carte est expirée
|
||||
*/
|
||||
public boolean isCardExpired() {
|
||||
if (!isCard() || cardExpMonth == null || cardExpYear == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime cardExpiry = LocalDateTime.of(cardExpYear, cardExpMonth, 1, 0, 0)
|
||||
.plusMonths(1).minusDays(1); // Dernier jour du mois d'expiration
|
||||
|
||||
return cardExpiry.isBefore(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Une représentation textuelle masquée de la méthode de paiement
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
switch (type) {
|
||||
case CARD:
|
||||
if (cardBrand != null && cardLast4 != null) {
|
||||
return cardBrand.toUpperCase() + " **** **** **** " + cardLast4;
|
||||
}
|
||||
return "Carte bancaire";
|
||||
|
||||
case SEPA_DEBIT:
|
||||
if (bankName != null) {
|
||||
return "SEPA - " + bankName;
|
||||
}
|
||||
return "Prélèvement SEPA";
|
||||
|
||||
case BANK_TRANSFER:
|
||||
if (bankName != null) {
|
||||
return "Virement - " + bankName;
|
||||
}
|
||||
return "Virement bancaire";
|
||||
|
||||
case PAYPAL:
|
||||
return "PayPal";
|
||||
|
||||
case APPLE_PAY:
|
||||
return "Apple Pay";
|
||||
|
||||
case GOOGLE_PAY:
|
||||
return "Google Pay";
|
||||
|
||||
default:
|
||||
return type.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Une description de l'état de la méthode de paiement
|
||||
*/
|
||||
public String getStatusDescription() {
|
||||
if (isCard()) {
|
||||
if (isCardExpired()) {
|
||||
return "Carte expirée";
|
||||
} else if (isCardExpiringSoon()) {
|
||||
return "Carte expire bientôt";
|
||||
} else {
|
||||
return "Carte valide";
|
||||
}
|
||||
}
|
||||
return "Méthode active";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la méthode de paiement est fonctionnelle
|
||||
*/
|
||||
public boolean isFunctional() {
|
||||
if (isCard()) {
|
||||
return !isCardExpired();
|
||||
}
|
||||
// Pour les autres méthodes, on suppose qu'elles sont fonctionnelles
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
/**
|
||||
* Types de moyens de paiement supportés
|
||||
*/
|
||||
public enum PaymentType {
|
||||
/**
|
||||
* Carte bancaire (Visa, Mastercard, etc.)
|
||||
*/
|
||||
CARD,
|
||||
|
||||
/**
|
||||
* Prélèvement SEPA (Europe)
|
||||
*/
|
||||
SEPA_DEBIT,
|
||||
|
||||
/**
|
||||
* Virement bancaire
|
||||
*/
|
||||
BANK_TRANSFER,
|
||||
|
||||
/**
|
||||
* PayPal
|
||||
*/
|
||||
PAYPAL,
|
||||
|
||||
/**
|
||||
* Apple Pay
|
||||
*/
|
||||
APPLE_PAY,
|
||||
|
||||
/**
|
||||
* Google Pay
|
||||
*/
|
||||
GOOGLE_PAY
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Plan d'abonnement avec tarification et fonctionnalités
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Plan {
|
||||
private UUID id;
|
||||
private String name;
|
||||
private LicenseType type;
|
||||
private String stripePriceIdMonthly;
|
||||
private String stripePriceIdYearly;
|
||||
private BigDecimal monthlyPrice;
|
||||
private BigDecimal yearlyPrice;
|
||||
private Integer maxUsers;
|
||||
private Set<String> features;
|
||||
private Integer trialDurationDays;
|
||||
private boolean isActive;
|
||||
private Integer displayOrder;
|
||||
private Map<String, String> metadata;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* @return Le prix pour le cycle de facturation spécifié
|
||||
*/
|
||||
public BigDecimal getPriceForCycle(BillingCycle cycle) {
|
||||
return cycle.isMonthly() ? monthlyPrice : yearlyPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'ID de prix Stripe pour le cycle spécifié
|
||||
*/
|
||||
public String getStripePriceIdForCycle(BillingCycle cycle) {
|
||||
return cycle.isMonthly() ? stripePriceIdMonthly : stripePriceIdYearly;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si ce plan supporte les trials
|
||||
*/
|
||||
public boolean supportsTrials() {
|
||||
return trialDurationDays != null && trialDurationDays > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si ce plan a une limite d'utilisateurs
|
||||
*/
|
||||
public boolean hasUserLimit() {
|
||||
return type != null && type.hasUserLimit() && maxUsers != null && maxUsers > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si ce plan inclut la fonctionnalité spécifiée
|
||||
*/
|
||||
public boolean hasFeature(String feature) {
|
||||
return features != null && features.contains(feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le prix mensuel équivalent (même pour les plans annuels)
|
||||
*/
|
||||
public BigDecimal getMonthlyEquivalentPrice(BillingCycle cycle) {
|
||||
BigDecimal price = getPriceForCycle(cycle);
|
||||
return cycle.isYearly() ? price.divide(BigDecimal.valueOf(12), 2, java.math.RoundingMode.HALF_UP) : price;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le pourcentage d'économies sur le plan annuel vs mensuel
|
||||
*/
|
||||
public BigDecimal getYearlySavingsPercentage() {
|
||||
if (monthlyPrice == null || yearlyPrice == null || monthlyPrice.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
BigDecimal yearlyEquivalent = monthlyPrice.multiply(BigDecimal.valueOf(12));
|
||||
BigDecimal savings = yearlyEquivalent.subtract(yearlyPrice);
|
||||
return savings.divide(yearlyEquivalent, 4, java.math.RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
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.UUID;
|
||||
|
||||
/**
|
||||
* Fonctionnalité associée à un plan d'abonnement
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PlanFeature {
|
||||
private UUID id;
|
||||
private String featureCode;
|
||||
private String name;
|
||||
private String description;
|
||||
private boolean enabled;
|
||||
private Integer usageLimit;
|
||||
private String category;
|
||||
private Integer displayOrder;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* Fonctionnalités standard disponibles
|
||||
*/
|
||||
public static class Features {
|
||||
public static final String BASIC_QUOTES = "BASIC_QUOTES";
|
||||
public static final String ADVANCED_ANALYTICS = "ADVANCED_ANALYTICS";
|
||||
public static final String BULK_IMPORT = "BULK_IMPORT";
|
||||
public static final String API_ACCESS = "API_ACCESS";
|
||||
public static final String PRIORITY_SUPPORT = "PRIORITY_SUPPORT";
|
||||
public static final String CUSTOM_BRANDING = "CUSTOM_BRANDING";
|
||||
public static final String MULTI_CURRENCY = "MULTI_CURRENCY";
|
||||
public static final String DOCUMENT_TEMPLATES = "DOCUMENT_TEMPLATES";
|
||||
public static final String AUTOMATED_WORKFLOWS = "AUTOMATED_WORKFLOWS";
|
||||
public static final String TEAM_COLLABORATION = "TEAM_COLLABORATION";
|
||||
public static final String AUDIT_LOGS = "AUDIT_LOGS";
|
||||
public static final String CUSTOM_INTEGRATIONS = "CUSTOM_INTEGRATIONS";
|
||||
public static final String SLA_GUARANTEE = "SLA_GUARANTEE";
|
||||
public static final String DEDICATED_SUPPORT = "DEDICATED_SUPPORT";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette fonctionnalité a une limite d'usage
|
||||
*/
|
||||
public boolean hasUsageLimit() {
|
||||
return usageLimit != null && usageLimit > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette fonctionnalité est une fonctionnalité premium
|
||||
*/
|
||||
public boolean isPremiumFeature() {
|
||||
return Features.ADVANCED_ANALYTICS.equals(featureCode)
|
||||
|| Features.API_ACCESS.equals(featureCode)
|
||||
|| Features.CUSTOM_BRANDING.equals(featureCode)
|
||||
|| Features.AUTOMATED_WORKFLOWS.equals(featureCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette fonctionnalité est réservée à Enterprise
|
||||
*/
|
||||
public boolean isEnterpriseFeature() {
|
||||
return Features.CUSTOM_INTEGRATIONS.equals(featureCode)
|
||||
|| Features.SLA_GUARANTEE.equals(featureCode)
|
||||
|| Features.DEDICATED_SUPPORT.equals(featureCode)
|
||||
|| Features.AUDIT_LOGS.equals(featureCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return la catégorie de la fonctionnalité pour l'affichage
|
||||
*/
|
||||
public String getDisplayCategory() {
|
||||
if (category != null) {
|
||||
return category;
|
||||
}
|
||||
|
||||
// Catégories par défaut basées sur le code de fonctionnalité
|
||||
if (featureCode.contains("ANALYTICS") || featureCode.contains("AUDIT")) {
|
||||
return "Reporting & Analytics";
|
||||
} else if (featureCode.contains("SUPPORT") || featureCode.contains("SLA")) {
|
||||
return "Support";
|
||||
} else if (featureCode.contains("API") || featureCode.contains("INTEGRATION")) {
|
||||
return "Intégrations";
|
||||
} else if (featureCode.contains("BRANDING") || featureCode.contains("TEMPLATE")) {
|
||||
return "Personnalisation";
|
||||
} else if (featureCode.contains("TEAM") || featureCode.contains("COLLABORATION")) {
|
||||
return "Collaboration";
|
||||
} else {
|
||||
return "Général";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return une description enrichie de la fonctionnalité
|
||||
*/
|
||||
public String getEnhancedDescription() {
|
||||
StringBuilder desc = new StringBuilder();
|
||||
|
||||
if (description != null) {
|
||||
desc.append(description);
|
||||
}
|
||||
|
||||
if (hasUsageLimit()) {
|
||||
if (desc.length() > 0) {
|
||||
desc.append(" ");
|
||||
}
|
||||
desc.append("(Limite: ").append(usageLimit).append(")");
|
||||
}
|
||||
|
||||
return desc.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,145 @@
|
||||
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.UUID;
|
||||
|
||||
/**
|
||||
* Abonnement d'une entreprise avec intégration Stripe
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Subscription {
|
||||
private UUID id;
|
||||
private String stripeSubscriptionId;
|
||||
private String stripeCustomerId;
|
||||
private String stripePriceId;
|
||||
private SubscriptionStatus status;
|
||||
private LocalDateTime currentPeriodStart;
|
||||
private LocalDateTime currentPeriodEnd;
|
||||
private boolean cancelAtPeriodEnd;
|
||||
private PaymentMethod paymentMethod;
|
||||
private BillingCycle billingCycle;
|
||||
private LocalDateTime nextBillingDate;
|
||||
private LocalDateTime trialEndDate;
|
||||
private License license;
|
||||
private List<Invoice> invoices;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est actif
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return status == SubscriptionStatus.ACTIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est en période d'essai
|
||||
*/
|
||||
public boolean isTrialing() {
|
||||
return status == SubscriptionStatus.TRIALING;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est en retard de paiement
|
||||
*/
|
||||
public boolean isPastDue() {
|
||||
return status == SubscriptionStatus.PAST_DUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est annulé
|
||||
*/
|
||||
public boolean isCanceled() {
|
||||
return status == SubscriptionStatus.CANCELED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est programmé pour annulation
|
||||
*/
|
||||
public boolean isScheduledForCancellation() {
|
||||
return cancelAtPeriodEnd && isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement est encore dans la période d'essai
|
||||
*/
|
||||
public boolean isInTrialPeriod() {
|
||||
return trialEndDate != null && LocalDateTime.now().isBefore(trialEndDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours restants dans la période d'essai
|
||||
*/
|
||||
public long getDaysRemainingInTrial() {
|
||||
if (trialEndDate == null) return 0;
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (now.isAfter(trialEndDate)) return 0;
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(now, trialEndDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le nombre de jours jusqu'à la prochaine facturation
|
||||
*/
|
||||
public long getDaysUntilNextBilling() {
|
||||
if (nextBillingDate == null) return Long.MAX_VALUE;
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (now.isAfter(nextBillingDate)) return 0;
|
||||
|
||||
return java.time.temporal.ChronoUnit.DAYS.between(now, nextBillingDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement nécessite une attention (paiement échoué, etc.)
|
||||
*/
|
||||
public boolean requiresAttention() {
|
||||
return status == SubscriptionStatus.PAST_DUE
|
||||
|| status == SubscriptionStatus.UNPAID
|
||||
|| status == SubscriptionStatus.INCOMPLETE
|
||||
|| status == SubscriptionStatus.INCOMPLETE_EXPIRED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'abonnement peut être réactivé
|
||||
*/
|
||||
public boolean canBeReactivated() {
|
||||
return status == SubscriptionStatus.CANCELED
|
||||
|| status == SubscriptionStatus.PAST_DUE
|
||||
|| status == SubscriptionStatus.UNPAID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut de l'abonnement
|
||||
*/
|
||||
public void updateStatus(SubscriptionStatus newStatus) {
|
||||
this.status = newStatus;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Programme l'annulation de l'abonnement à la fin de la période
|
||||
*/
|
||||
public void scheduleForCancellation() {
|
||||
this.cancelAtPeriodEnd = true;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Annule la programmation d'annulation
|
||||
*/
|
||||
public void unscheduleForCancellation() {
|
||||
this.cancelAtPeriodEnd = false;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.dh7789dev.xpeditis.dto.app;
|
||||
|
||||
/**
|
||||
* Statuts possibles d'un abonnement
|
||||
*/
|
||||
public enum SubscriptionStatus {
|
||||
/**
|
||||
* Période d'essai en cours
|
||||
*/
|
||||
TRIALING,
|
||||
|
||||
/**
|
||||
* Abonnement actif avec paiements à jour
|
||||
*/
|
||||
ACTIVE,
|
||||
|
||||
/**
|
||||
* Paiement en retard mais dans la période de grâce
|
||||
*/
|
||||
PAST_DUE,
|
||||
|
||||
/**
|
||||
* Abonnement annulé par l'utilisateur ou le système
|
||||
*/
|
||||
CANCELED,
|
||||
|
||||
/**
|
||||
* Abonnement suspendu pour impayé (après période de grâce)
|
||||
*/
|
||||
UNPAID,
|
||||
|
||||
/**
|
||||
* Abonnement en cours de traitement (création/modification)
|
||||
*/
|
||||
INCOMPLETE,
|
||||
|
||||
/**
|
||||
* Abonnement en attente de première action de paiement
|
||||
*/
|
||||
INCOMPLETE_EXPIRED
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
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,20 +1,51 @@
|
||||
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.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserAccount {
|
||||
private String username;
|
||||
private UUID id;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String email;
|
||||
private Email email;
|
||||
private String username;
|
||||
private String password;
|
||||
private String role; // or "ADMIN"
|
||||
private PhoneNumber phoneNumber;
|
||||
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 License license;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
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,41 +1,70 @@
|
||||
package com.dh7789dev.xpeditis.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import com.dh7789dev.xpeditis.dto.app.AuthProvider;
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@Data
|
||||
@Builder(toBuilder = true)
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Accessors(chain = true)
|
||||
public class RegisterRequest {
|
||||
|
||||
@NotBlank
|
||||
@NotBlank(message = "First name is required")
|
||||
@Size(max = 50, message = "First name must not exceed 50 characters")
|
||||
String firstName;
|
||||
|
||||
@NotBlank
|
||||
@NotBlank(message = "Last name is required")
|
||||
@Size(max = 50, message = "Last name must not exceed 50 characters")
|
||||
String lastName;
|
||||
|
||||
@NotBlank
|
||||
String username;
|
||||
|
||||
@NotBlank
|
||||
@Email(message = "Invalid email format")
|
||||
@NotBlank(message = "Email is required")
|
||||
String email;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 50, message = "Username must not exceed 50 characters")
|
||||
String username;
|
||||
|
||||
@Pattern(
|
||||
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$",
|
||||
message = "Password must be at least 8 characters with uppercase, lowercase and digit"
|
||||
)
|
||||
String password;
|
||||
|
||||
@NotBlank
|
||||
String phone;
|
||||
String confirmPassword;
|
||||
|
||||
String company_uuid = "";
|
||||
@NotBlank(message = "Phone number is required")
|
||||
@Pattern(
|
||||
regexp = "^\\+?[1-9]\\d{1,14}$",
|
||||
message = "Invalid phone number format"
|
||||
)
|
||||
String phoneNumber;
|
||||
|
||||
String company_name;
|
||||
@NotBlank(message = "Company name is required")
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
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,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
@ -12,6 +13,7 @@ import lombok.experimental.FieldDefaults;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@ -25,12 +27,22 @@ public class AuthenticationResponse {
|
||||
@JsonProperty("refresh_token")
|
||||
String refreshToken;
|
||||
|
||||
@JsonProperty("token_type")
|
||||
@Builder.Default
|
||||
String tokenType = "Bearer";
|
||||
|
||||
@JsonProperty("expires_in")
|
||||
long expiresIn;
|
||||
|
||||
@JsonProperty("created_at")
|
||||
Date createdAt;
|
||||
|
||||
@JsonProperty("expires_at")
|
||||
Date expiresAt;
|
||||
|
||||
@JsonProperty("user")
|
||||
UserResponse user;
|
||||
|
||||
@JsonProperty("error_message")
|
||||
String error;
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
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;
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
package com.dh7789dev.xpeditis.dto.valueobject;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Value Object représentant un montant monétaire avec devise
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Money {
|
||||
private BigDecimal amount;
|
||||
private String currency;
|
||||
|
||||
/**
|
||||
* Crée un montant en euros
|
||||
*/
|
||||
public static Money euros(BigDecimal amount) {
|
||||
return new Money(amount, "EUR");
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un montant en euros à partir d'un double
|
||||
*/
|
||||
public static Money euros(double amount) {
|
||||
return euros(BigDecimal.valueOf(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un montant en dollars
|
||||
*/
|
||||
public static Money dollars(BigDecimal amount) {
|
||||
return new Money(amount, "USD");
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un montant en dollars à partir d'un double
|
||||
*/
|
||||
public static Money dollars(double amount) {
|
||||
return dollars(BigDecimal.valueOf(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un montant zéro dans la devise spécifiée
|
||||
*/
|
||||
public static Money zero(String currency) {
|
||||
return new Money(BigDecimal.ZERO, currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un montant zéro en euros
|
||||
*/
|
||||
public static Money zeroEuros() {
|
||||
return zero("EUR");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si le montant est positif
|
||||
*/
|
||||
public boolean isPositive() {
|
||||
return amount != null && amount.compareTo(BigDecimal.ZERO) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si le montant est zéro
|
||||
*/
|
||||
public boolean isZero() {
|
||||
return amount == null || amount.compareTo(BigDecimal.ZERO) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si le montant est négatif
|
||||
*/
|
||||
public boolean isNegative() {
|
||||
return amount != null && amount.compareTo(BigDecimal.ZERO) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additionne deux montants (doivent avoir la même devise)
|
||||
*/
|
||||
public Money add(Money other) {
|
||||
if (!Objects.equals(this.currency, other.currency)) {
|
||||
throw new IllegalArgumentException("Cannot add amounts with different currencies: "
|
||||
+ this.currency + " and " + other.currency);
|
||||
}
|
||||
return new Money(this.amount.add(other.amount), this.currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soustrait un montant (doivent avoir la même devise)
|
||||
*/
|
||||
public Money subtract(Money other) {
|
||||
if (!Objects.equals(this.currency, other.currency)) {
|
||||
throw new IllegalArgumentException("Cannot subtract amounts with different currencies: "
|
||||
+ this.currency + " and " + other.currency);
|
||||
}
|
||||
return new Money(this.amount.subtract(other.amount), this.currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplie par un facteur
|
||||
*/
|
||||
public Money multiply(BigDecimal factor) {
|
||||
return new Money(this.amount.multiply(factor), this.currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplie par un facteur double
|
||||
*/
|
||||
public Money multiply(double factor) {
|
||||
return multiply(BigDecimal.valueOf(factor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Divise par un diviseur
|
||||
*/
|
||||
public Money divide(BigDecimal divisor) {
|
||||
return new Money(this.amount.divide(divisor, 2, java.math.RoundingMode.HALF_UP), this.currency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Divise par un diviseur double
|
||||
*/
|
||||
public Money divide(double divisor) {
|
||||
return divide(BigDecimal.valueOf(divisor));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return le montant formaté avec la devise
|
||||
*/
|
||||
public String toDisplayString() {
|
||||
if (amount == null) return "0.00 " + (currency != null ? currency : "");
|
||||
|
||||
switch (currency) {
|
||||
case "EUR":
|
||||
return String.format("%.2f €", amount);
|
||||
case "USD":
|
||||
return String.format("$%.2f", amount);
|
||||
default:
|
||||
return String.format("%.2f %s", amount, currency);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Money money = (Money) o;
|
||||
return Objects.equals(amount, money.amount) && Objects.equals(currency, money.currency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(amount, currency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toDisplayString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
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,6 +62,10 @@
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-crypto</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-tx</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -1,26 +1,244 @@
|
||||
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.response.AuthenticationResponse;
|
||||
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.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Transactional
|
||||
public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
|
||||
private final AuthenticationRepository authenticationRepository;
|
||||
|
||||
public AuthenticationServiceImpl(AuthenticationRepository authenticationRepository) {
|
||||
this.authenticationRepository = authenticationRepository;
|
||||
}
|
||||
private final UserRepository userRepository;
|
||||
private final OAuth2Provider oAuth2Provider;
|
||||
private final CompanyService companyService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user