feature devis
This commit is contained in:
parent
d1be066a20
commit
f31f1b6c69
@ -421,6 +421,100 @@
|
||||
}
|
||||
],
|
||||
"description": "Endpoints de gestion des utilisateurs - profil, mot de passe, suppression de compte"
|
||||
},
|
||||
{
|
||||
"name": "📋 Devis Transport",
|
||||
"item": [
|
||||
{
|
||||
"name": "Calculer Devis (3 offres automatiques)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"typeService\": \"EXPORT\",\n \"incoterm\": \"FOB\",\n \"typeLivraison\": \"PORTE_A_PORTE\",\n \"depart\": {\n \"ville\": \"Marseille\",\n \"codePostal\": \"13000\",\n \"pays\": \"France\",\n \"coordonneesGps\": \"43.2965,5.3698\"\n },\n \"arrivee\": {\n \"ville\": \"Shanghai\",\n \"codePostal\": \"200000\",\n \"pays\": \"Chine\",\n \"coordonneesGps\": \"31.2304,121.4737\"\n },\n \"douaneImportExport\": \"EXPORT\",\n \"eur1Import\": false,\n \"eur1Export\": true,\n \"colisages\": [\n {\n \"type\": \"PALETTE\",\n \"quantite\": 3,\n \"longueur\": 120.0,\n \"largeur\": 80.0,\n \"hauteur\": 160.0,\n \"poids\": 750.5,\n \"gerbable\": true\n },\n {\n \"type\": \"CAISSE\",\n \"quantite\": 2,\n \"longueur\": 100.0,\n \"largeur\": 60.0,\n \"hauteur\": 120.0,\n \"poids\": 450.2,\n \"gerbable\": false\n }\n ],\n \"marchandiseDangereuse\": {\n \"presente\": true,\n \"classe\": \"3\",\n \"numeroOnu\": \"UN1263\",\n \"description\": \"Peintures inflammables\"\n },\n \"manutentionParticuliere\": {\n \"hayon\": true,\n \"sangles\": true,\n \"couvertureThermique\": false,\n \"autres\": \"Manutention précautionneuse requise\"\n },\n \"produitsReglementes\": {\n \"alimentaire\": false,\n \"pharmaceutique\": false,\n \"autres\": null\n },\n \"servicesAdditionnels\": {\n \"rendezVousLivraison\": true,\n \"documentT1\": false,\n \"stopDouane\": true,\n \"assistanceExport\": true,\n \"assurance\": true,\n \"valeurDeclaree\": 15000.0\n },\n \"dateEnlevement\": \"2025-01-15\",\n \"dateLivraison\": \"2025-02-20\",\n \"nomClient\": \"Maritime Solutions SAS\",\n \"emailClient\": \"contact@maritime-solutions.fr\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/devis/calculer",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "devis", "calculer"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Valider Demande de Devis",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"typeService\": \"EXPORT\",\n \"incoterm\": \"FOB\",\n \"depart\": {\n \"ville\": \"Marseille\",\n \"codePostal\": \"13000\",\n \"pays\": \"France\"\n },\n \"arrivee\": {\n \"ville\": \"Shanghai\",\n \"codePostal\": \"200000\",\n \"pays\": \"Chine\"\n },\n \"colisages\": [\n {\n \"type\": \"CAISSE\",\n \"quantite\": 2,\n \"poids\": 150.5,\n \"longueur\": 120.0,\n \"largeur\": 80.0,\n \"hauteur\": 100.0,\n \"gerbable\": false\n }\n ],\n \"marchandiseDangereuse\": null,\n \"nomClient\": \"Test Client\",\n \"emailClient\": \"test@test.com\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/devis/valider",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "devis", "valider"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Calculer Devis - Exemple Simple",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"typeService\": \"EXPORT\",\n \"incoterm\": \"FOB\",\n \"depart\": {\n \"ville\": \"Marseille\",\n \"codePostal\": \"13000\",\n \"pays\": \"France\"\n },\n \"arrivee\": {\n \"ville\": \"Shanghai\",\n \"codePostal\": \"200000\",\n \"pays\": \"Chine\"\n },\n \"colisages\": [\n {\n \"type\": \"CAISSE\",\n \"quantite\": 2,\n \"poids\": 150.5,\n \"longueur\": 120.0,\n \"largeur\": 80.0,\n \"hauteur\": 100.0,\n \"gerbable\": false\n }\n ],\n \"marchandiseDangereuse\": null,\n \"nomClient\": \"Test Client\",\n \"emailClient\": \"test@test.com\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/devis/calculer",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "devis", "calculer"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Calculer Devis - Avec Marchandises Dangereuses",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"typeService\": \"EXPORT\",\n \"incoterm\": \"FOB\",\n \"depart\": {\n \"ville\": \"Marseille\",\n \"codePostal\": \"13000\",\n \"pays\": \"France\"\n },\n \"arrivee\": {\n \"ville\": \"Hong Kong\",\n \"codePostal\": \"999077\",\n \"pays\": \"Hong Kong\"\n },\n \"colisages\": [\n {\n \"type\": \"PALETTE\",\n \"quantite\": 5,\n \"poids\": 890.0,\n \"longueur\": 120.0,\n \"largeur\": 80.0,\n \"hauteur\": 200.0,\n \"gerbable\": true\n }\n ],\n \"marchandiseDangereuse\": {\n \"presente\": true,\n \"classe\": \"3\",\n \"numeroOnu\": \"UN1263\",\n \"description\": \"Produits chimiques inflammables\"\n },\n \"servicesAdditionnels\": {\n \"assurance\": true,\n \"valeurDeclaree\": 25000.0,\n \"rendezVousLivraison\": true,\n \"assistanceExport\": true\n },\n \"nomClient\": \"Chemical Industries Ltd\",\n \"emailClient\": \"export@chemical-industries.com\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/devis/calculer",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "devis", "calculer"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
],
|
||||
"description": "API de calcul automatisé de devis transport maritime - génère 3 offres (Rapide, Standard, Économique) basées sur les grilles tarifaires LESCHACO"
|
||||
}
|
||||
],
|
||||
"event": [
|
||||
|
||||
@ -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,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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -41,6 +41,8 @@ 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 = {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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,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,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,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,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,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
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,478 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DevisCalculServiceImpl implements DevisCalculService {
|
||||
|
||||
private final GrilleTarifaireService grilleTarifaireService;
|
||||
|
||||
private static final BigDecimal COEFFICIENT_POIDS_VOLUMETRIQUE = new BigDecimal("250"); // 250kg/m³
|
||||
|
||||
@Override
|
||||
public ReponseDevis calculerTroisOffres(DemandeDevis demandeDevis) {
|
||||
log.info("Calcul des 3 offres pour demande devis client: {}", demandeDevis.getNomClient());
|
||||
|
||||
validerDemandeDevis(demandeDevis);
|
||||
|
||||
// Calculer le colisage résumé
|
||||
ReponseDevis.ColisageResume colisageResume = calculerColisageResume(demandeDevis);
|
||||
|
||||
// Trouver les grilles applicables
|
||||
List<GrilleTarifaire> grillesApplicables = grilleTarifaireService.trouverGrillesApplicables(demandeDevis);
|
||||
|
||||
if (grillesApplicables.isEmpty()) {
|
||||
throw new IllegalStateException("Aucune grille tarifaire applicable pour cette demande");
|
||||
}
|
||||
|
||||
// Générer les 3 offres
|
||||
List<OffreCalculee> offres = genererTroisOffres(demandeDevis, grillesApplicables, colisageResume);
|
||||
|
||||
// Déterminer la recommandation
|
||||
ReponseDevis.Recommandation recommandation = determinerRecommandation(offres);
|
||||
|
||||
return new ReponseDevis(
|
||||
generateDemandeId(),
|
||||
new ReponseDevis.ClientInfo(demandeDevis.getNomClient(), demandeDevis.getEmailClient()),
|
||||
mapperDetailsTransport(demandeDevis),
|
||||
colisageResume,
|
||||
offres,
|
||||
recommandation,
|
||||
getMentionsLegales()
|
||||
);
|
||||
}
|
||||
|
||||
private List<OffreCalculee> genererTroisOffres(
|
||||
DemandeDevis demande,
|
||||
List<GrilleTarifaire> grilles,
|
||||
ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
List<OffreCalculee> offres = new ArrayList<>();
|
||||
|
||||
// Pour chaque type de service (Rapide, Standard, Économique)
|
||||
for (GrilleTarifaire.ServiceType serviceType : GrilleTarifaire.ServiceType.values()) {
|
||||
|
||||
// Filtrer les grilles par type de service
|
||||
List<GrilleTarifaire> grillesService = grilles.stream()
|
||||
.filter(g -> g.getServiceType() == serviceType)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Si pas de grilles spécifiques, utiliser les grilles standard
|
||||
if (grillesService.isEmpty()) {
|
||||
grillesService = grilles.stream()
|
||||
.filter(g -> g.getServiceType() == GrilleTarifaire.ServiceType.STANDARD)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Calculer la meilleure offre pour ce type de service
|
||||
OffreCalculee meilleureOffre = calculerMeilleureOffre(demande, grillesService, serviceType, colisage);
|
||||
|
||||
if (meilleureOffre != null) {
|
||||
offres.add(appliquerAjustementParType(meilleureOffre, serviceType));
|
||||
}
|
||||
}
|
||||
|
||||
return offres;
|
||||
}
|
||||
|
||||
private OffreCalculee calculerMeilleureOffre(
|
||||
DemandeDevis demande,
|
||||
List<GrilleTarifaire> grilles,
|
||||
GrilleTarifaire.ServiceType serviceType,
|
||||
ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
OffreCalculee meilleureOffre = null;
|
||||
BigDecimal prixMinimal = BigDecimal.valueOf(Double.MAX_VALUE);
|
||||
|
||||
for (GrilleTarifaire grille : grilles) {
|
||||
try {
|
||||
OffreCalculee offre = calculerOffreGrille(demande, grille, serviceType, colisage);
|
||||
|
||||
if (offre != null && offre.getPrixTotal().compareTo(prixMinimal) < 0) {
|
||||
prixMinimal = offre.getPrixTotal();
|
||||
meilleureOffre = offre;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Erreur lors du calcul avec la grille {}: {}", grille.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return meilleureOffre;
|
||||
}
|
||||
|
||||
private OffreCalculee calculerOffreGrille(
|
||||
DemandeDevis demande,
|
||||
GrilleTarifaire grille,
|
||||
GrilleTarifaire.ServiceType serviceType,
|
||||
ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
// 1. Calculer le fret de base
|
||||
BigDecimal fretBase = calculerFretBase(grille, colisage);
|
||||
|
||||
// 2. Calculer les frais fixes obligatoires
|
||||
Map<String, BigDecimal> fraisFixes = calculerFraisFixes(grille, demande, colisage);
|
||||
|
||||
// 3. Calculer les surcharges marchandises dangereuses
|
||||
BigDecimal surchargeDangereuse = calculerSurchargeDangereuse(grille, demande, colisage);
|
||||
|
||||
// 4. Calculer les services optionnels demandés
|
||||
Map<String, BigDecimal> servicesOptionnels = calculerServicesOptionnels(grille, demande, colisage);
|
||||
|
||||
// 5. Calculer le prix total
|
||||
BigDecimal prixTotal = fretBase
|
||||
.add(fraisFixes.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add))
|
||||
.add(surchargeDangereuse)
|
||||
.add(servicesOptionnels.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
|
||||
// Créer les détails du prix
|
||||
OffreCalculee.DetailPrix detailPrix = new OffreCalculee.DetailPrix();
|
||||
detailPrix.setFretBase(fretBase);
|
||||
detailPrix.setFraisFixes(fraisFixes);
|
||||
detailPrix.setServicesOptionnels(servicesOptionnels);
|
||||
detailPrix.setSurchargeDangereuse(surchargeDangereuse);
|
||||
detailPrix.setCoefficientService(BigDecimal.ONE);
|
||||
|
||||
// Créer l'offre
|
||||
OffreCalculee offre = new OffreCalculee();
|
||||
offre.setType(serviceType.name());
|
||||
offre.setPrixTotal(prixTotal);
|
||||
offre.setDevise(grille.getDevise());
|
||||
offre.setTransitTime(formatTransitTime(grille));
|
||||
offre.setTransporteur(grille.getTransporteur());
|
||||
offre.setModeTransport(grille.getModeTransport().name());
|
||||
offre.setServicesInclus(getServicesInclus(serviceType));
|
||||
offre.setDetailPrix(detailPrix);
|
||||
offre.setValidite(LocalDate.now().plusDays(30));
|
||||
offre.setConditions(getConditions(serviceType));
|
||||
|
||||
return offre;
|
||||
}
|
||||
|
||||
private BigDecimal calculerFretBase(GrilleTarifaire grille, ReponseDevis.ColisageResume colisage) {
|
||||
|
||||
// Trouver le tarif applicable selon le poids taxable
|
||||
TarifFret tarifApplicable = grille.getTarifsFret().stream()
|
||||
.filter(t -> {
|
||||
BigDecimal poidsTaxable = BigDecimal.valueOf(colisage.getPoidsTaxable());
|
||||
return (t.getPoidsMin() == null || t.getPoidsMin().compareTo(poidsTaxable) <= 0) &&
|
||||
(t.getPoidsMax() == null || t.getPoidsMax().compareTo(poidsTaxable) >= 0);
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (tarifApplicable == null) {
|
||||
throw new IllegalStateException("Aucun tarif applicable pour le poids taxable: " + colisage.getPoidsTaxable());
|
||||
}
|
||||
|
||||
// Calculer le coût selon l'unité de facturation
|
||||
BigDecimal cout = BigDecimal.ZERO;
|
||||
|
||||
switch (tarifApplicable.getUniteFacturation()) {
|
||||
case KG:
|
||||
cout = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(tarifApplicable.getTauxUnitaire());
|
||||
break;
|
||||
case M3:
|
||||
cout = BigDecimal.valueOf(colisage.getVolumeTotal()).multiply(tarifApplicable.getTauxUnitaire());
|
||||
break;
|
||||
case COLIS:
|
||||
cout = BigDecimal.valueOf(colisage.getNombreColis()).multiply(tarifApplicable.getTauxUnitaire());
|
||||
break;
|
||||
case LS:
|
||||
cout = tarifApplicable.getTauxUnitaire();
|
||||
break;
|
||||
}
|
||||
|
||||
// Appliquer le minimum de facturation si défini
|
||||
if (tarifApplicable.getMinimumFacturation() != null &&
|
||||
cout.compareTo(tarifApplicable.getMinimumFacturation()) < 0) {
|
||||
cout = tarifApplicable.getMinimumFacturation();
|
||||
}
|
||||
|
||||
return cout.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> calculerFraisFixes(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
Map<String, BigDecimal> fraisFixes = new HashMap<>();
|
||||
|
||||
for (FraisAdditionnels frais : grille.getFraisAdditionnels()) {
|
||||
if (frais.getObligatoire()) {
|
||||
BigDecimal montant = calculerMontantFrais(frais, demande, colisage);
|
||||
fraisFixes.put(frais.getTypeFrais(), montant);
|
||||
}
|
||||
}
|
||||
|
||||
return fraisFixes;
|
||||
}
|
||||
|
||||
private BigDecimal calculerSurchargeDangereuse(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
if (demande.getMarchandiseDangereuse() == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
SurchargeDangereuse surcharge = grille.getSurchargesDangereuses().stream()
|
||||
.filter(s -> s.getClasseAdr().equals(demande.getMarchandiseDangereuse().getClasse()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (surcharge == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
BigDecimal montant = BigDecimal.ZERO;
|
||||
|
||||
switch (surcharge.getUniteFacturation()) {
|
||||
case KG:
|
||||
montant = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(surcharge.getSurcharge());
|
||||
break;
|
||||
case COLIS:
|
||||
montant = BigDecimal.valueOf(colisage.getNombreColis()).multiply(surcharge.getSurcharge());
|
||||
break;
|
||||
case LS:
|
||||
montant = surcharge.getSurcharge();
|
||||
break;
|
||||
}
|
||||
|
||||
if (surcharge.getMinimum() != null && montant.compareTo(surcharge.getMinimum()) < 0) {
|
||||
montant = surcharge.getMinimum();
|
||||
}
|
||||
|
||||
return montant.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> calculerServicesOptionnels(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
Map<String, BigDecimal> services = new HashMap<>();
|
||||
|
||||
// Assurance
|
||||
if (demande.getServicesAdditionnels() != null && demande.getServicesAdditionnels().getAssurance()) {
|
||||
FraisAdditionnels fraisAssurance = grille.getFraisAdditionnels().stream()
|
||||
.filter(f -> "ASSURANCE".equals(f.getTypeFrais()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (fraisAssurance != null) {
|
||||
BigDecimal montant = calculerMontantFrais(fraisAssurance, demande, colisage);
|
||||
services.put("assurance", montant);
|
||||
}
|
||||
}
|
||||
|
||||
// Hayon
|
||||
if (demande.getManutentionParticuliere() != null && demande.getManutentionParticuliere().getHayon()) {
|
||||
FraisAdditionnels fraisHayon = grille.getFraisAdditionnels().stream()
|
||||
.filter(f -> "HAYON".equals(f.getTypeFrais()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
if (fraisHayon != null) {
|
||||
BigDecimal montant = calculerMontantFrais(fraisHayon, demande, colisage);
|
||||
services.put("hayon", montant);
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private BigDecimal calculerMontantFrais(FraisAdditionnels frais, DemandeDevis demande, ReponseDevis.ColisageResume colisage) {
|
||||
BigDecimal montant = BigDecimal.ZERO;
|
||||
|
||||
switch (frais.getUniteFacturation()) {
|
||||
case LS:
|
||||
montant = frais.getMontant();
|
||||
break;
|
||||
case KG:
|
||||
montant = BigDecimal.valueOf(colisage.getPoidsTaxable()).multiply(frais.getMontant());
|
||||
break;
|
||||
case M3:
|
||||
montant = BigDecimal.valueOf(colisage.getVolumeTotal()).multiply(frais.getMontant());
|
||||
break;
|
||||
case POURCENTAGE:
|
||||
// Pourcentage du fret de base - à implémenter selon le contexte
|
||||
montant = frais.getMontant();
|
||||
break;
|
||||
}
|
||||
|
||||
if (frais.getMontantMinimum() != null && montant.compareTo(frais.getMontantMinimum()) < 0) {
|
||||
montant = frais.getMontantMinimum();
|
||||
}
|
||||
|
||||
return montant.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private OffreCalculee appliquerAjustementParType(OffreCalculee offre, GrilleTarifaire.ServiceType serviceType) {
|
||||
BigDecimal coefficient = getCoefficient(serviceType);
|
||||
Double reductionTransit = getReductionTransitTime(serviceType);
|
||||
|
||||
// Appliquer le coefficient au prix total
|
||||
BigDecimal prixAjuste = offre.getPrixTotal().multiply(coefficient).setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
// Ajuster le détail des prix
|
||||
offre.getDetailPrix().setCoefficientService(coefficient);
|
||||
|
||||
// Mettre à jour le prix total
|
||||
offre.setPrixTotal(prixAjuste);
|
||||
|
||||
// Ajuster le transit time si défini
|
||||
if (reductionTransit != null) {
|
||||
String transitTimeAjuste = ajusterTransitTime(offre.getTransitTime(), reductionTransit);
|
||||
offre.setTransitTime(transitTimeAjuste);
|
||||
}
|
||||
|
||||
return offre;
|
||||
}
|
||||
|
||||
private BigDecimal getCoefficient(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return new BigDecimal("1.15"); // +15%
|
||||
case STANDARD:
|
||||
return BigDecimal.ONE;
|
||||
case ECONOMIQUE:
|
||||
return new BigDecimal("0.85"); // -15%
|
||||
default:
|
||||
return BigDecimal.ONE;
|
||||
}
|
||||
}
|
||||
|
||||
private Double getReductionTransitTime(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return 0.7; // -30%
|
||||
case STANDARD:
|
||||
return null; // Pas de changement
|
||||
case ECONOMIQUE:
|
||||
return 1.3; // +30%
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ReponseDevis.ColisageResume calculerColisageResume(DemandeDevis demande) {
|
||||
double poidsTotal = demande.getColisages().stream()
|
||||
.mapToDouble(c -> c.getPoids() * c.getQuantite())
|
||||
.sum();
|
||||
|
||||
double volumeTotal = demande.getColisages().stream()
|
||||
.mapToDouble(c -> c.getVolume() * c.getQuantite())
|
||||
.sum();
|
||||
|
||||
int nombreColis = demande.getColisages().stream()
|
||||
.mapToInt(DemandeDevis.Colisage::getQuantite)
|
||||
.sum();
|
||||
|
||||
// Calculer le poids taxable (max entre poids réel et poids volumétrique)
|
||||
double poidsVolumetrique = volumeTotal * COEFFICIENT_POIDS_VOLUMETRIQUE.doubleValue();
|
||||
double poidsTaxable = Math.max(poidsTotal, poidsVolumetrique);
|
||||
|
||||
return new ReponseDevis.ColisageResume(nombreColis, poidsTotal, volumeTotal, poidsTaxable);
|
||||
}
|
||||
|
||||
private List<String> getServicesInclus(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return Arrays.asList("Suivi en temps réel", "Assurance de base", "Service express");
|
||||
case STANDARD:
|
||||
return Arrays.asList("Suivi standard");
|
||||
case ECONOMIQUE:
|
||||
return Collections.emptyList();
|
||||
default:
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getConditions(GrilleTarifaire.ServiceType serviceType) {
|
||||
switch (serviceType) {
|
||||
case RAPIDE:
|
||||
return Arrays.asList("Prix valable sous réserve d'espace disponible", "Marchandise prête à l'enlèvement");
|
||||
case STANDARD:
|
||||
return Arrays.asList("Prix standard selon grille tarifaire", "Délais indicatifs");
|
||||
case ECONOMIQUE:
|
||||
return Arrays.asList("Tarif économique avec délais étendus", "Services minimaux inclus");
|
||||
default:
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private ReponseDevis.Recommandation determinerRecommandation(List<OffreCalculee> offres) {
|
||||
// Par défaut, recommander l'offre STANDARD
|
||||
return new ReponseDevis.Recommandation("STANDARD", "Meilleur rapport qualité/prix/délai");
|
||||
}
|
||||
|
||||
private ReponseDevis.DetailsTransport mapperDetailsTransport(DemandeDevis demande) {
|
||||
ReponseDevis.DetailsTransport.AdresseInfo depart = new ReponseDevis.DetailsTransport.AdresseInfo(
|
||||
demande.getDepart().getVille() + ", " + demande.getDepart().getCodePostal() + ", " + demande.getDepart().getPays(),
|
||||
demande.getDepart().getCoordonneesGps()
|
||||
);
|
||||
|
||||
ReponseDevis.DetailsTransport.AdresseInfo arrivee = new ReponseDevis.DetailsTransport.AdresseInfo(
|
||||
demande.getArrivee().getVille() + ", " + demande.getArrivee().getCodePostal() + ", " + demande.getArrivee().getPays(),
|
||||
demande.getArrivee().getCoordonneesGps()
|
||||
);
|
||||
|
||||
return new ReponseDevis.DetailsTransport(demande.getTypeService(), demande.getIncoterm(), depart, arrivee);
|
||||
}
|
||||
|
||||
private String formatTransitTime(GrilleTarifaire grille) {
|
||||
if (grille.getTransitTimeMin() != null && grille.getTransitTimeMax() != null) {
|
||||
return grille.getTransitTimeMin() + "-" + grille.getTransitTimeMax() + " jours";
|
||||
}
|
||||
return "À définir";
|
||||
}
|
||||
|
||||
private String ajusterTransitTime(String transitTime, double coefficient) {
|
||||
// Extraction des nombres du format "25-30 jours" et application du coefficient
|
||||
if (transitTime.contains("-")) {
|
||||
String[] parts = transitTime.split("-");
|
||||
try {
|
||||
int min = (int) (Integer.parseInt(parts[0]) * coefficient);
|
||||
int max = (int) (Integer.parseInt(parts[1].split(" ")[0]) * coefficient);
|
||||
return min + "-" + max + " jours";
|
||||
} catch (NumberFormatException e) {
|
||||
return transitTime;
|
||||
}
|
||||
}
|
||||
return transitTime;
|
||||
}
|
||||
|
||||
private String generateDemandeId() {
|
||||
return "DEV-" + LocalDate.now().getYear() + "-" + String.format("%06d", new Random().nextInt(999999));
|
||||
}
|
||||
|
||||
private List<String> getMentionsLegales() {
|
||||
return Arrays.asList(
|
||||
"Prix hors taxes applicables",
|
||||
"Conditions générales de vente applicables",
|
||||
"Devis valable 30 jours"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validerDemandeDevis(DemandeDevis demandeDevis) {
|
||||
if (demandeDevis == null) {
|
||||
throw new IllegalArgumentException("La demande de devis ne peut pas être nulle");
|
||||
}
|
||||
|
||||
if (demandeDevis.getDepart() == null || demandeDevis.getArrivee() == null) {
|
||||
throw new IllegalArgumentException("Les adresses de départ et d'arrivée sont obligatoires");
|
||||
}
|
||||
|
||||
if (demandeDevis.getColisages() == null || demandeDevis.getColisages().isEmpty()) {
|
||||
throw new IllegalArgumentException("Au moins un colisage doit être défini");
|
||||
}
|
||||
|
||||
for (DemandeDevis.Colisage colisage : demandeDevis.getColisages()) {
|
||||
if (colisage.getPoids() == null || colisage.getPoids() <= 0) {
|
||||
throw new IllegalArgumentException("Le poids de chaque colisage doit être supérieur à 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,426 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.DemandeDevis;
|
||||
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class GrilleTarifaireServiceImpl implements GrilleTarifaireService {
|
||||
|
||||
private final GrilleTarifaireRepository grilleTarifaireRepository;
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> trouverGrillesApplicables(DemandeDevis demandeDevis) {
|
||||
log.info("Recherche des grilles tarifaires applicables pour {} -> {}",
|
||||
demandeDevis.getDepart().getPays(),
|
||||
demandeDevis.getArrivee().getPays());
|
||||
|
||||
LocalDate dateValidite = demandeDevis.getDateEnlevement() != null
|
||||
? demandeDevis.getDateEnlevement()
|
||||
: LocalDate.now();
|
||||
|
||||
List<GrilleTarifaire> grilles = grilleTarifaireRepository.findGrillesApplicables(
|
||||
demandeDevis.getTypeService(),
|
||||
demandeDevis.getDepart().getPays(),
|
||||
demandeDevis.getArrivee().getPays(),
|
||||
dateValidite
|
||||
);
|
||||
|
||||
// Filtrer par ville si spécifiée
|
||||
if (demandeDevis.getDepart().getVille() != null || demandeDevis.getArrivee().getVille() != null) {
|
||||
grilles = grilles.stream()
|
||||
.filter(g -> isVilleCompatible(g, demandeDevis))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// Filtrer par incoterm si spécifié
|
||||
if (demandeDevis.getIncoterm() != null) {
|
||||
grilles = grilles.stream()
|
||||
.filter(g -> g.getIncoterm() == null || g.getIncoterm().equals(demandeDevis.getIncoterm()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
log.info("Trouvé {} grille(s) applicables", grilles.size());
|
||||
return grilles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrilleTarifaire sauvegarderGrille(GrilleTarifaire grilleTarifaire) {
|
||||
log.info("Sauvegarde de la grille tarifaire: {}", grilleTarifaire.getNomGrille());
|
||||
|
||||
validerGrilleTarifaire(grilleTarifaire);
|
||||
|
||||
return grilleTarifaireRepository.save(grilleTarifaire);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrilleTarifaire trouverParId(Long id) {
|
||||
log.debug("Recherche de la grille tarifaire avec l'ID: {}", id);
|
||||
|
||||
return grilleTarifaireRepository.findById(id)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void supprimerGrille(Long id) {
|
||||
log.info("Suppression de la grille tarifaire avec l'ID: {}", id);
|
||||
|
||||
if (!grilleTarifaireRepository.existsById(id)) {
|
||||
throw new IllegalArgumentException("Aucune grille tarifaire trouvée avec l'ID: " + id);
|
||||
}
|
||||
|
||||
grilleTarifaireRepository.deleteById(id);
|
||||
}
|
||||
|
||||
private boolean isVilleCompatible(GrilleTarifaire grille, DemandeDevis demande) {
|
||||
// Si la grille n'a pas de ville spécifiée, elle est compatible avec toutes les villes
|
||||
boolean origineCompatible = grille.getOrigineVille() == null ||
|
||||
grille.getOrigineVille().equalsIgnoreCase(demande.getDepart().getVille());
|
||||
|
||||
boolean destinationCompatible = grille.getDestinationVille() == null ||
|
||||
grille.getDestinationVille().equalsIgnoreCase(demande.getArrivee().getVille());
|
||||
|
||||
return origineCompatible && destinationCompatible;
|
||||
}
|
||||
|
||||
private void validerGrilleTarifaire(GrilleTarifaire grille) {
|
||||
if (grille == null) {
|
||||
throw new IllegalArgumentException("La grille tarifaire ne peut pas être nulle");
|
||||
}
|
||||
|
||||
if (grille.getNomGrille() == null || grille.getNomGrille().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le nom de la grille est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getTransporteur() == null || grille.getTransporteur().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le transporteur est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getTypeService() == null) {
|
||||
throw new IllegalArgumentException("Le type de service est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getOriginePays() == null || grille.getOriginePays().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le pays d'origine est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getDestinationPays() == null || grille.getDestinationPays().trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Le pays de destination est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getValiditeDebut() == null) {
|
||||
throw new IllegalArgumentException("La date de début de validité est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getValiditeFin() == null) {
|
||||
throw new IllegalArgumentException("La date de fin de validité est obligatoire");
|
||||
}
|
||||
|
||||
if (grille.getValiditeDebut().isAfter(grille.getValiditeFin())) {
|
||||
throw new IllegalArgumentException("La date de début doit être antérieure à la date de fin");
|
||||
}
|
||||
|
||||
// Validation des tarifs de fret
|
||||
if (grille.getTarifsFret() == null || grille.getTarifsFret().isEmpty()) {
|
||||
throw new IllegalArgumentException("Au moins un tarif de fret doit être défini");
|
||||
}
|
||||
|
||||
// Validation des codes pays (format ISO 3166-1 alpha-3)
|
||||
if (grille.getOriginePays().length() != 3) {
|
||||
throw new IllegalArgumentException("Le code pays d'origine doit être au format ISO 3166-1 alpha-3 (3 caractères)");
|
||||
}
|
||||
|
||||
if (grille.getDestinationPays().length() != 3) {
|
||||
throw new IllegalArgumentException("Le code pays de destination doit être au format ISO 3166-1 alpha-3 (3 caractères)");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> importerDepuisCsv(MultipartFile file, String mode) throws IOException {
|
||||
log.info("Import CSV - Fichier: {}, Mode: {}", file.getOriginalFilename(), mode);
|
||||
|
||||
if ("REPLACE".equalsIgnoreCase(mode)) {
|
||||
log.info("Mode REPLACE - Suppression de toutes les grilles existantes");
|
||||
grilleTarifaireRepository.deleteAll();
|
||||
}
|
||||
|
||||
List<GrilleTarifaire> grillesImportees = new ArrayList<>();
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), "UTF-8"))) {
|
||||
String headerLine = reader.readLine();
|
||||
if (headerLine == null) {
|
||||
throw new IllegalArgumentException("Le fichier CSV est vide");
|
||||
}
|
||||
|
||||
String[] headers = headerLine.split(",");
|
||||
log.debug("En-têtes CSV: {}", Arrays.toString(headers));
|
||||
|
||||
String line;
|
||||
int lineNumber = 1;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
lineNumber++;
|
||||
if (line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
GrilleTarifaire grille = parseCsvLine(line, headers, lineNumber);
|
||||
if (grille != null) {
|
||||
validerGrilleTarifaire(grille);
|
||||
GrilleTarifaire grilleSauvegardee = grilleTarifaireRepository.save(grille);
|
||||
grillesImportees.add(grilleSauvegardee);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur ligne {}: {}", lineNumber, e.getMessage());
|
||||
throw new IllegalArgumentException("Erreur ligne " + lineNumber + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Import CSV terminé - {} grilles importées", grillesImportees.size());
|
||||
return grillesImportees;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> importerDepuisExcel(MultipartFile file, String sheetName, String mode) throws IOException {
|
||||
log.info("Import Excel - Fichier: {}, Feuille: {}, Mode: {}", file.getOriginalFilename(), sheetName, mode);
|
||||
|
||||
// Pour cette version simplifiée, nous convertissons Excel vers CSV puis utilisons le parser CSV
|
||||
// Dans une implémentation complète, nous utiliserions Apache POI
|
||||
|
||||
throw new UnsupportedOperationException("L'import Excel n'est pas encore implémenté. Utilisez le format CSV.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> importerDepuisJson(List<GrilleTarifaire> grilles, String mode) {
|
||||
log.info("Import JSON - {} grilles, Mode: {}", grilles.size(), mode);
|
||||
|
||||
if ("REPLACE".equalsIgnoreCase(mode)) {
|
||||
log.info("Mode REPLACE - Suppression de toutes les grilles existantes");
|
||||
grilleTarifaireRepository.deleteAll();
|
||||
}
|
||||
|
||||
List<GrilleTarifaire> grillesImportees = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < grilles.size(); i++) {
|
||||
try {
|
||||
GrilleTarifaire grille = grilles.get(i);
|
||||
validerGrilleTarifaire(grille);
|
||||
|
||||
// Si la grille a un ID et existe déjà, mise à jour. Sinon, création.
|
||||
if (grille.getId() != null && grilleTarifaireRepository.existsById(grille.getId())) {
|
||||
log.debug("Mise à jour grille existante ID: {}", grille.getId());
|
||||
} else {
|
||||
grille.setId(null); // Force la création d'une nouvelle grille
|
||||
}
|
||||
|
||||
GrilleTarifaire grilleSauvegardee = grilleTarifaireRepository.save(grille);
|
||||
grillesImportees.add(grilleSauvegardee);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du traitement de la grille #{}: {}", i + 1, e.getMessage());
|
||||
throw new IllegalArgumentException("Erreur grille #" + (i + 1) + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Import JSON terminé - {} grilles importées", grillesImportees.size());
|
||||
return grillesImportees;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> listerGrilles(int page, int size, String transporteur, String paysOrigine, String paysDestination) {
|
||||
log.debug("Listing grilles - page: {}, size: {}, transporteur: {}, origine: {}, destination: {}",
|
||||
page, size, transporteur, paysOrigine, paysDestination);
|
||||
|
||||
// Pour cette implémentation simplifiée, nous récupérons toutes les grilles et filtrons
|
||||
// Dans une implémentation complète, nous utiliserions des requêtes JPA avec Specification
|
||||
|
||||
List<GrilleTarifaire> toutes = grilleTarifaireRepository.findAll();
|
||||
|
||||
// Application des filtres
|
||||
return toutes.stream()
|
||||
.filter(g -> transporteur == null || g.getTransporteur().toLowerCase().contains(transporteur.toLowerCase()))
|
||||
.filter(g -> paysOrigine == null || g.getOriginePays().equalsIgnoreCase(paysOrigine))
|
||||
.filter(g -> paysDestination == null || g.getDestinationPays().equalsIgnoreCase(paysDestination))
|
||||
.skip((long) page * size)
|
||||
.limit(size)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> validerFichier(MultipartFile file) throws IOException {
|
||||
log.info("Validation fichier - Nom: {}, Taille: {} bytes", file.getOriginalFilename(), file.getSize());
|
||||
|
||||
Map<String, Object> resultat = new HashMap<>();
|
||||
List<String> erreurs = new ArrayList<>();
|
||||
List<String> avertissements = new ArrayList<>();
|
||||
int lignesValides = 0;
|
||||
int lignesTotal = 0;
|
||||
|
||||
String filename = file.getOriginalFilename().toLowerCase();
|
||||
resultat.put("nomFichier", file.getOriginalFilename());
|
||||
resultat.put("tailleFichier", file.getSize());
|
||||
resultat.put("typeFichier", filename.endsWith(".csv") ? "CSV" : filename.endsWith(".xlsx") ? "Excel" : "Inconnu");
|
||||
|
||||
if (filename.endsWith(".csv")) {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), "UTF-8"))) {
|
||||
String headerLine = reader.readLine();
|
||||
lignesTotal++;
|
||||
|
||||
if (headerLine == null) {
|
||||
erreurs.add("Le fichier est vide");
|
||||
} else {
|
||||
String[] headers = headerLine.split(",");
|
||||
resultat.put("nombreColonnes", headers.length);
|
||||
resultat.put("colonnes", Arrays.asList(headers));
|
||||
|
||||
// Vérification des colonnes obligatoires
|
||||
List<String> colonnesObligatoires = Arrays.asList(
|
||||
"nomGrille", "transporteur", "typeService", "originePays", "destinationPays",
|
||||
"validiteDebut", "validiteFin"
|
||||
);
|
||||
|
||||
for (String colonne : colonnesObligatoires) {
|
||||
boolean trouve = false;
|
||||
for (String header : headers) {
|
||||
if (header.trim().equalsIgnoreCase(colonne)) {
|
||||
trouve = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!trouve) {
|
||||
erreurs.add("Colonne obligatoire manquante: " + colonne);
|
||||
}
|
||||
}
|
||||
|
||||
String line;
|
||||
int lineNumber = 1;
|
||||
|
||||
while ((line = reader.readLine()) != null && lineNumber <= 100) { // Limite pour la validation
|
||||
lineNumber++;
|
||||
lignesTotal++;
|
||||
|
||||
if (line.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
GrilleTarifaire grille = parseCsvLine(line, headers, lineNumber);
|
||||
if (grille != null) {
|
||||
validerGrilleTarifaire(grille);
|
||||
lignesValides++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
erreurs.add("Ligne " + lineNumber + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (lineNumber > 100) {
|
||||
avertissements.add("Validation limitée aux 100 premières lignes de données");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
erreurs.add("Format de fichier non supporté. Seuls les fichiers CSV sont actuellement supportés.");
|
||||
}
|
||||
|
||||
resultat.put("lignesTotal", lignesTotal);
|
||||
resultat.put("lignesValides", lignesValides);
|
||||
resultat.put("erreurs", erreurs);
|
||||
resultat.put("avertissements", avertissements);
|
||||
resultat.put("valide", erreurs.isEmpty());
|
||||
|
||||
return resultat;
|
||||
}
|
||||
|
||||
private GrilleTarifaire parseCsvLine(String line, String[] headers, int lineNumber) {
|
||||
String[] values = line.split(",", -1); // -1 pour conserver les valeurs vides
|
||||
|
||||
if (values.length != headers.length) {
|
||||
throw new IllegalArgumentException("Nombre de colonnes incorrect. Attendu: " + headers.length + ", trouvé: " + values.length);
|
||||
}
|
||||
|
||||
GrilleTarifaire grille = new GrilleTarifaire();
|
||||
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
String header = headers[i].trim();
|
||||
String value = values[i].trim();
|
||||
|
||||
if (value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (header.toLowerCase()) {
|
||||
case "nomgrille":
|
||||
grille.setNomGrille(value);
|
||||
break;
|
||||
case "transporteur":
|
||||
grille.setTransporteur(value);
|
||||
break;
|
||||
case "typeservice":
|
||||
grille.setTypeService(GrilleTarifaire.TypeService.valueOf(value.toUpperCase()));
|
||||
break;
|
||||
case "originepays":
|
||||
grille.setOriginePays(value);
|
||||
break;
|
||||
case "destinationpays":
|
||||
grille.setDestinationPays(value);
|
||||
break;
|
||||
case "origineville":
|
||||
grille.setOrigineVille(value);
|
||||
break;
|
||||
case "destinationville":
|
||||
grille.setDestinationVille(value);
|
||||
break;
|
||||
case "validitedebut":
|
||||
grille.setValiditeDebut(LocalDate.parse(value, dateFormatter));
|
||||
break;
|
||||
case "validiteefin":
|
||||
grille.setValiditeFin(LocalDate.parse(value, dateFormatter));
|
||||
break;
|
||||
case "incoterm":
|
||||
grille.setIncoterm(value);
|
||||
break;
|
||||
case "modetransport":
|
||||
grille.setModeTransport(GrilleTarifaire.ModeTransport.valueOf(value.toUpperCase()));
|
||||
break;
|
||||
case "actif":
|
||||
grille.setActif(Boolean.parseBoolean(value));
|
||||
break;
|
||||
case "devisebase":
|
||||
grille.setDeviseBase(value);
|
||||
break;
|
||||
case "commentaires":
|
||||
grille.setCommentaires(value);
|
||||
break;
|
||||
// Pour les tarifs de fret et autres listes, nous aurions besoin d'un format plus complexe
|
||||
// Dans cette implémentation simplifiée, nous les ignorons
|
||||
default:
|
||||
log.debug("Colonne ignorée: {}", header);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("Erreur dans la colonne '" + header + "': " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return grille;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,277 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.*;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("DevisCalculService - Tests unitaires")
|
||||
class DevisCalculServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private GrilleTarifaireService grilleTarifaireService;
|
||||
|
||||
private DevisCalculServiceImpl devisCalculService;
|
||||
private DemandeDevis demandeDevisValide;
|
||||
private GrilleTarifaire grilleTarifaireStandard;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
devisCalculService = new DevisCalculServiceImpl(grilleTarifaireService);
|
||||
|
||||
// Créer une demande de devis valide pour les tests
|
||||
demandeDevisValide = creerDemandeDevisValide();
|
||||
|
||||
// Créer une grille tarifaire standard pour les tests
|
||||
grilleTarifaireStandard = creerGrilleTarifaireStandard();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit calculer 3 offres avec succès")
|
||||
void doitCalculerTroisOffresAvecSucces() {
|
||||
// Given
|
||||
when(grilleTarifaireService.trouverGrillesApplicables(any(DemandeDevis.class)))
|
||||
.thenReturn(Arrays.asList(grilleTarifaireStandard));
|
||||
|
||||
// When
|
||||
ReponseDevis reponse = devisCalculService.calculerTroisOffres(demandeDevisValide);
|
||||
|
||||
// Then
|
||||
assertThat(reponse).isNotNull();
|
||||
assertThat(reponse.getOffres()).hasSize(3);
|
||||
|
||||
// Vérifier que les 3 types d'offres sont présents
|
||||
List<String> typesOffres = reponse.getOffres().stream()
|
||||
.map(OffreCalculee::getType)
|
||||
.toList();
|
||||
|
||||
assertThat(typesOffres).containsExactlyInAnyOrder("RAPIDE", "STANDARD", "ECONOMIQUE");
|
||||
|
||||
// Vérifier que l'offre rapide est la plus chère
|
||||
OffreCalculee offreRapide = reponse.getOffres().stream()
|
||||
.filter(o -> "RAPIDE".equals(o.getType()))
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
OffreCalculee offreStandard = reponse.getOffres().stream()
|
||||
.filter(o -> "STANDARD".equals(o.getType()))
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
OffreCalculee offreEconomique = reponse.getOffres().stream()
|
||||
.filter(o -> "ECONOMIQUE".equals(o.getType()))
|
||||
.findFirst().orElseThrow();
|
||||
|
||||
assertThat(offreRapide.getPrixTotal()).isGreaterThan(offreStandard.getPrixTotal());
|
||||
assertThat(offreStandard.getPrixTotal()).isGreaterThan(offreEconomique.getPrixTotal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit calculer correctement le colisage résumé")
|
||||
void doitCalculerCorrectementColisageResume() {
|
||||
// Given
|
||||
when(grilleTarifaireService.trouverGrillesApplicables(any(DemandeDevis.class)))
|
||||
.thenReturn(Arrays.asList(grilleTarifaireStandard));
|
||||
|
||||
DemandeDevis demande = creerDemandeAvecColisage();
|
||||
|
||||
// When
|
||||
ReponseDevis reponse = devisCalculService.calculerTroisOffres(demande);
|
||||
|
||||
// Then
|
||||
ReponseDevis.ColisageResume colisage = reponse.getColisageResume();
|
||||
assertThat(colisage.getNombreColis()).isEqualTo(3); // 2 + 1
|
||||
assertThat(colisage.getPoidsTotal()).isEqualTo(350.0); // (100*2) + (150*1)
|
||||
assertThat(colisage.getVolumeTotal()).isEqualTo(0.35); // (0.1*2) + (0.15*1)
|
||||
|
||||
// Le poids taxable doit être le max entre poids réel et poids volumétrique
|
||||
double poidsVolumetrique = 0.35 * 250; // 87.5 kg
|
||||
assertThat(colisage.getPoidsTaxable()).isEqualTo(350.0); // Poids réel > poids volumétrique
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si aucune grille applicable")
|
||||
void doitLeverExceptionSiAucuneGrilleApplicable() {
|
||||
// Given
|
||||
when(grilleTarifaireService.trouverGrillesApplicables(any(DemandeDevis.class)))
|
||||
.thenReturn(Arrays.asList());
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.calculerTroisOffres(demandeDevisValide))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Aucune grille tarifaire applicable");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit valider correctement une demande de devis valide")
|
||||
void doitValiderCorrectementDemandeValide() {
|
||||
// When & Then
|
||||
assertThatCode(() -> devisCalculService.validerDemandeDevis(demandeDevisValide))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si demande de devis nulle")
|
||||
void doitLeverExceptionSiDemandeNulle() {
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("ne peut pas être nulle");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si adresses manquantes")
|
||||
void doitLeverExceptionSiAdressesManquantes() {
|
||||
// Given
|
||||
DemandeDevis demande = creerDemandeDevisValide();
|
||||
demande.setDepart(null);
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(demande))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("adresses de départ et d'arrivée sont obligatoires");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si aucun colisage")
|
||||
void doitLeverExceptionSiAucunColisage() {
|
||||
// Given
|
||||
DemandeDevis demande = creerDemandeDevisValide();
|
||||
demande.setColisages(Arrays.asList());
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(demande))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Au moins un colisage doit être défini");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit lever une exception si poids invalide")
|
||||
void doitLeverExceptionSiPoidsInvalide() {
|
||||
// Given
|
||||
DemandeDevis demande = creerDemandeDevisValide();
|
||||
demande.getColisages().get(0).setPoids(0.0);
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> devisCalculService.validerDemandeDevis(demande))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Le poids de chaque colisage doit être supérieur à 0");
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Méthodes utilitaires pour créer les objets de test
|
||||
// ================================
|
||||
|
||||
private DemandeDevis creerDemandeDevisValide() {
|
||||
DemandeDevis demande = new DemandeDevis();
|
||||
demande.setTypeService("EXPORT");
|
||||
demande.setIncoterm("FOB");
|
||||
demande.setTypeLivraison("Door to Door");
|
||||
demande.setNomClient("Test Client");
|
||||
demande.setEmailClient("test@example.com");
|
||||
|
||||
// Adresses
|
||||
DemandeDevis.AdresseTransport depart = new DemandeDevis.AdresseTransport();
|
||||
depart.setVille("Lyon");
|
||||
depart.setCodePostal("69000");
|
||||
depart.setPays("FRA");
|
||||
demande.setDepart(depart);
|
||||
|
||||
DemandeDevis.AdresseTransport arrivee = new DemandeDevis.AdresseTransport();
|
||||
arrivee.setVille("Shanghai");
|
||||
arrivee.setCodePostal("200000");
|
||||
arrivee.setPays("CHN");
|
||||
demande.setArrivee(arrivee);
|
||||
|
||||
// Colisage simple
|
||||
DemandeDevis.Colisage colisage = new DemandeDevis.Colisage();
|
||||
colisage.setType(DemandeDevis.Colisage.TypeColisage.COLIS);
|
||||
colisage.setQuantite(1);
|
||||
colisage.setLongueur(50.0);
|
||||
colisage.setLargeur(40.0);
|
||||
colisage.setHauteur(30.0);
|
||||
colisage.setPoids(25.0);
|
||||
|
||||
demande.setColisages(Arrays.asList(colisage));
|
||||
demande.setDateEnlevement(LocalDate.now().plusDays(7));
|
||||
|
||||
return demande;
|
||||
}
|
||||
|
||||
private DemandeDevis creerDemandeAvecColisage() {
|
||||
DemandeDevis demande = creerDemandeDevisValide();
|
||||
|
||||
// Premier colisage
|
||||
DemandeDevis.Colisage colisage1 = new DemandeDevis.Colisage();
|
||||
colisage1.setType(DemandeDevis.Colisage.TypeColisage.COLIS);
|
||||
colisage1.setQuantite(2);
|
||||
colisage1.setLongueur(50.0);
|
||||
colisage1.setLargeur(40.0);
|
||||
colisage1.setHauteur(50.0); // Volume = 0.1 m³
|
||||
colisage1.setPoids(100.0);
|
||||
|
||||
// Deuxième colisage
|
||||
DemandeDevis.Colisage colisage2 = new DemandeDevis.Colisage();
|
||||
colisage2.setType(DemandeDevis.Colisage.TypeColisage.PALETTE);
|
||||
colisage2.setQuantite(1);
|
||||
colisage2.setLongueur(120.0);
|
||||
colisage2.setLargeur(80.0);
|
||||
colisage2.setHauteur(150.0); // Volume = 0.15 m³
|
||||
colisage2.setPoids(150.0);
|
||||
colisage2.setGerbable(true);
|
||||
|
||||
demande.setColisages(Arrays.asList(colisage1, colisage2));
|
||||
|
||||
return demande;
|
||||
}
|
||||
|
||||
private GrilleTarifaire creerGrilleTarifaireStandard() {
|
||||
GrilleTarifaire grille = new GrilleTarifaire();
|
||||
grille.setId(1L);
|
||||
grille.setNomGrille("Test Grille Standard");
|
||||
grille.setTransporteur("LESCHACO");
|
||||
grille.setTypeService(GrilleTarifaire.TypeService.EXPORT);
|
||||
grille.setOriginePays("FRA");
|
||||
grille.setDestinationPays("CHN");
|
||||
grille.setModeTransport(GrilleTarifaire.ModeTransport.MARITIME);
|
||||
grille.setServiceType(GrilleTarifaire.ServiceType.STANDARD);
|
||||
grille.setTransitTimeMin(25);
|
||||
grille.setTransitTimeMax(30);
|
||||
grille.setValiditeDebut(LocalDate.now().minusDays(30));
|
||||
grille.setValiditeFin(LocalDate.now().plusDays(60));
|
||||
grille.setDevise("EUR");
|
||||
|
||||
// Tarif de fret
|
||||
TarifFret tarif = new TarifFret();
|
||||
tarif.setPoidsMin(BigDecimal.ZERO);
|
||||
tarif.setPoidsMax(BigDecimal.valueOf(1000));
|
||||
tarif.setTauxUnitaire(BigDecimal.valueOf(2.5));
|
||||
tarif.setUniteFacturation(TarifFret.UniteFacturation.KG);
|
||||
tarif.setMinimumFacturation(BigDecimal.valueOf(100));
|
||||
|
||||
grille.setTarifsFret(Arrays.asList(tarif));
|
||||
|
||||
// Frais additionnels obligatoires
|
||||
FraisAdditionnels fraisDoc = new FraisAdditionnels();
|
||||
fraisDoc.setTypeFrais("DOCUMENTATION");
|
||||
fraisDoc.setMontant(BigDecimal.valueOf(32));
|
||||
fraisDoc.setUniteFacturation(FraisAdditionnels.UniteFacturation.LS);
|
||||
fraisDoc.setObligatoire(true);
|
||||
|
||||
grille.setFraisAdditionnels(Arrays.asList(fraisDoc));
|
||||
grille.setSurchargesDangereuses(Arrays.asList());
|
||||
|
||||
return grille;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package com.dh7789dev.xpeditis;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface GrilleTarifaireRepository {
|
||||
|
||||
/**
|
||||
* Trouve toutes les grilles tarifaires valides pour une route et période donnée
|
||||
*
|
||||
* @param typeService IMPORT ou EXPORT
|
||||
* @param originePays pays d'origine
|
||||
* @param destinationPays pays de destination
|
||||
* @param dateValidite date à laquelle la grille doit être valide
|
||||
* @return liste des grilles correspondantes
|
||||
*/
|
||||
List<GrilleTarifaire> findGrillesApplicables(
|
||||
String typeService,
|
||||
String originePays,
|
||||
String destinationPays,
|
||||
LocalDate dateValidite
|
||||
);
|
||||
|
||||
/**
|
||||
* Trouve toutes les grilles tarifaires d'un type de service spécifique
|
||||
*
|
||||
* @param typeService IMPORT ou EXPORT
|
||||
* @param serviceType RAPIDE, STANDARD ou ECONOMIQUE
|
||||
* @param originePays pays d'origine
|
||||
* @param destinationPays pays de destination
|
||||
* @param dateValidite date de validité
|
||||
* @return liste des grilles correspondantes
|
||||
*/
|
||||
List<GrilleTarifaire> findByServiceTypeAndRoute(
|
||||
String typeService,
|
||||
String serviceType,
|
||||
String originePays,
|
||||
String destinationPays,
|
||||
LocalDate dateValidite
|
||||
);
|
||||
|
||||
/**
|
||||
* Sauvegarde une grille tarifaire
|
||||
*
|
||||
* @param grilleTarifaire la grille à sauvegarder
|
||||
* @return la grille sauvegardée avec son ID généré
|
||||
*/
|
||||
GrilleTarifaire save(GrilleTarifaire grilleTarifaire);
|
||||
|
||||
/**
|
||||
* Trouve une grille tarifaire par son ID
|
||||
*
|
||||
* @param id l'identifiant de la grille
|
||||
* @return la grille trouvée ou Optional.empty()
|
||||
*/
|
||||
Optional<GrilleTarifaire> findById(Long id);
|
||||
|
||||
/**
|
||||
* Supprime une grille tarifaire
|
||||
*
|
||||
* @param id l'identifiant de la grille à supprimer
|
||||
*/
|
||||
void deleteById(Long id);
|
||||
|
||||
/**
|
||||
* Vérifie si une grille tarifaire existe
|
||||
*
|
||||
* @param id l'identifiant de la grille
|
||||
* @return true si la grille existe
|
||||
*/
|
||||
boolean existsById(Long id);
|
||||
|
||||
/**
|
||||
* Supprime toutes les grilles tarifaires
|
||||
*/
|
||||
void deleteAll();
|
||||
|
||||
/**
|
||||
* Trouve toutes les grilles tarifaires avec pagination et filtres
|
||||
*
|
||||
* @param page numéro de page
|
||||
* @param size taille de la page
|
||||
* @param transporteur filtre par transporteur (optionnel)
|
||||
* @param paysOrigine filtre par pays d'origine (optionnel)
|
||||
* @param paysDestination filtre par pays de destination (optionnel)
|
||||
* @return liste des grilles correspondantes
|
||||
*/
|
||||
List<GrilleTarifaire> findAllWithFilters(int page, int size, String transporteur, String paysOrigine, String paysDestination);
|
||||
|
||||
/**
|
||||
* Sauvegarde une liste de grilles tarifaires
|
||||
*
|
||||
* @param grilles la liste des grilles à sauvegarder
|
||||
* @return la liste des grilles sauvegardées
|
||||
*/
|
||||
List<GrilleTarifaire> saveAll(List<GrilleTarifaire> grilles);
|
||||
|
||||
/**
|
||||
* Trouve toutes les grilles tarifaires
|
||||
*
|
||||
* @return la liste de toutes les grilles
|
||||
*/
|
||||
List<GrilleTarifaire> findAll();
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
package com.dh7789dev.xpeditis.dao;
|
||||
|
||||
import com.dh7789dev.xpeditis.GrilleTarifaireRepository;
|
||||
import com.dh7789dev.xpeditis.dto.app.GrilleTarifaire;
|
||||
import com.dh7789dev.xpeditis.entity.GrilleTarifaireEntity;
|
||||
import com.dh7789dev.xpeditis.mapper.GrilleTarifaireMapper;
|
||||
import com.dh7789dev.xpeditis.repository.GrilleTarifaireJpaRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class GrilleTarifaireDao implements GrilleTarifaireRepository {
|
||||
|
||||
private final GrilleTarifaireJpaRepository jpaRepository;
|
||||
private final GrilleTarifaireMapper mapper;
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> findGrillesApplicables(
|
||||
String typeService,
|
||||
String originePays,
|
||||
String destinationPays,
|
||||
LocalDate dateValidite) {
|
||||
|
||||
log.debug("Recherche des grilles applicables: {} {} -> {} à la date {}",
|
||||
typeService, originePays, destinationPays, dateValidite);
|
||||
|
||||
GrilleTarifaireEntity.TypeService typeServiceEnum = parseTypeService(typeService);
|
||||
|
||||
List<GrilleTarifaireEntity> entities = jpaRepository.findGrillesApplicables(
|
||||
typeServiceEnum, originePays, destinationPays, dateValidite);
|
||||
|
||||
log.debug("Trouvé {} grilles dans la base de données", entities.size());
|
||||
|
||||
return entities.stream()
|
||||
.map(mapper::entityToDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> findByServiceTypeAndRoute(
|
||||
String typeService,
|
||||
String serviceType,
|
||||
String originePays,
|
||||
String destinationPays,
|
||||
LocalDate dateValidite) {
|
||||
|
||||
GrilleTarifaireEntity.TypeService typeServiceEnum = parseTypeService(typeService);
|
||||
GrilleTarifaireEntity.ServiceType serviceTypeEnum = parseServiceType(serviceType);
|
||||
|
||||
List<GrilleTarifaireEntity> entities = jpaRepository.findByServiceTypeAndRoute(
|
||||
typeServiceEnum, serviceTypeEnum, originePays, destinationPays, dateValidite);
|
||||
|
||||
return entities.stream()
|
||||
.map(mapper::entityToDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GrilleTarifaire save(GrilleTarifaire grilleTarifaire) {
|
||||
log.debug("Sauvegarde de la grille tarifaire: {}", grilleTarifaire.getNomGrille());
|
||||
|
||||
GrilleTarifaireEntity entity = mapper.dtoToEntity(grilleTarifaire);
|
||||
GrilleTarifaireEntity savedEntity = jpaRepository.save(entity);
|
||||
|
||||
return mapper.entityToDto(savedEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GrilleTarifaire> findById(Long id) {
|
||||
log.debug("Recherche de la grille tarifaire par ID: {}", id);
|
||||
|
||||
return jpaRepository.findById(id)
|
||||
.map(mapper::entityToDto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(Long id) {
|
||||
log.info("Suppression de la grille tarifaire avec l'ID: {}", id);
|
||||
jpaRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsById(Long id) {
|
||||
return jpaRepository.existsById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll() {
|
||||
log.info("Suppression de toutes les grilles tarifaires");
|
||||
jpaRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> findAllWithFilters(int page, int size, String transporteur, String paysOrigine, String paysDestination) {
|
||||
log.debug("Recherche des grilles avec filtres - page: {}, size: {}, transporteur: {}, origine: {}, destination: {}",
|
||||
page, size, transporteur, paysOrigine, paysDestination);
|
||||
|
||||
// Simple implementation - for more complex filtering, we would use Spring Data specifications
|
||||
List<GrilleTarifaireEntity> entities = jpaRepository.findAll();
|
||||
|
||||
return entities.stream()
|
||||
.filter(entity -> transporteur == null || transporteur.isEmpty() ||
|
||||
entity.getTransporteur().equalsIgnoreCase(transporteur))
|
||||
.filter(entity -> paysOrigine == null || paysOrigine.isEmpty() ||
|
||||
entity.getOriginePays().equalsIgnoreCase(paysOrigine))
|
||||
.filter(entity -> paysDestination == null || paysDestination.isEmpty() ||
|
||||
entity.getDestinationPays().equalsIgnoreCase(paysDestination))
|
||||
.skip(page * size)
|
||||
.limit(size)
|
||||
.map(mapper::entityToDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> saveAll(List<GrilleTarifaire> grilles) {
|
||||
log.info("Sauvegarde de {} grilles tarifaires", grilles.size());
|
||||
|
||||
List<GrilleTarifaireEntity> entities = grilles.stream()
|
||||
.map(mapper::dtoToEntity)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<GrilleTarifaireEntity> savedEntities = jpaRepository.saveAll(entities);
|
||||
|
||||
return savedEntities.stream()
|
||||
.map(mapper::entityToDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GrilleTarifaire> findAll() {
|
||||
log.debug("Recherche de toutes les grilles tarifaires");
|
||||
|
||||
List<GrilleTarifaireEntity> entities = jpaRepository.findAll();
|
||||
|
||||
return entities.stream()
|
||||
.map(mapper::entityToDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private GrilleTarifaireEntity.TypeService parseTypeService(String typeService) {
|
||||
try {
|
||||
return GrilleTarifaireEntity.TypeService.valueOf(typeService.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Type de service invalide: {}", typeService);
|
||||
throw new IllegalArgumentException("Type de service invalide: " + typeService);
|
||||
}
|
||||
}
|
||||
|
||||
private GrilleTarifaireEntity.ServiceType parseServiceType(String serviceType) {
|
||||
try {
|
||||
return GrilleTarifaireEntity.ServiceType.valueOf(serviceType.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Service type invalide: {}", serviceType);
|
||||
throw new IllegalArgumentException("Service type invalide: " + serviceType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@FieldNameConstants
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Table(name = "frais_additionnels",
|
||||
indexes = {
|
||||
@Index(name = "idx_grille_type", columnList = "grille_id, type_frais")
|
||||
})
|
||||
public class FraisAdditionnelsEntity extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "grille_id", nullable = false)
|
||||
private GrilleTarifaireEntity grille;
|
||||
|
||||
@Column(name = "type_frais", nullable = false, length = 50)
|
||||
private String typeFrais;
|
||||
|
||||
@Column(name = "description", length = 200)
|
||||
private String description;
|
||||
|
||||
@Column(name = "montant", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal montant;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "unite_facturation", nullable = false)
|
||||
private UniteFacturation uniteFacturation;
|
||||
|
||||
@Column(name = "montant_minimum", precision = 10, scale = 2)
|
||||
private BigDecimal montantMinimum;
|
||||
|
||||
@Column(name = "obligatoire")
|
||||
private Boolean obligatoire = false;
|
||||
|
||||
@Column(name = "applicable_marchandise_dangereuse")
|
||||
private Boolean applicableMarchandiseDangereuse = false;
|
||||
|
||||
public enum UniteFacturation {
|
||||
LS, KG, M3, PALETTE, POURCENTAGE
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@FieldNameConstants
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Table(name = "grilles_tarifaires")
|
||||
public class GrilleTarifaireEntity extends BaseEntity {
|
||||
|
||||
|
||||
@Column(name = "nom_grille", nullable = false, length = 100)
|
||||
private String nomGrille;
|
||||
|
||||
@Column(name = "transporteur", nullable = false, length = 50)
|
||||
private String transporteur;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "type_service", nullable = false)
|
||||
private TypeService typeService;
|
||||
|
||||
@Column(name = "origine_pays", nullable = false, length = 3)
|
||||
private String originePays;
|
||||
|
||||
@Column(name = "origine_ville", length = 100)
|
||||
private String origineVille;
|
||||
|
||||
@Column(name = "origine_port_code", length = 10)
|
||||
private String originePortCode;
|
||||
|
||||
@Column(name = "destination_pays", nullable = false, length = 3)
|
||||
private String destinationPays;
|
||||
|
||||
@Column(name = "destination_ville", length = 100)
|
||||
private String destinationVille;
|
||||
|
||||
@Column(name = "destination_port_code", length = 10)
|
||||
private String destinationPortCode;
|
||||
|
||||
@Column(name = "incoterm", length = 10)
|
||||
private String incoterm;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "mode_transport")
|
||||
private ModeTransport modeTransport;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "service_type")
|
||||
private ServiceType serviceType;
|
||||
|
||||
@Column(name = "transit_time_min")
|
||||
private Integer transitTimeMin;
|
||||
|
||||
@Column(name = "transit_time_max")
|
||||
private Integer transitTimeMax;
|
||||
|
||||
@Column(name = "validite_debut", nullable = false)
|
||||
private LocalDate validiteDebut;
|
||||
|
||||
@Column(name = "validite_fin", nullable = false)
|
||||
private LocalDate validiteFin;
|
||||
|
||||
@Column(name = "devise", length = 3)
|
||||
private String devise = "EUR";
|
||||
|
||||
@Column(name = "actif")
|
||||
private Boolean actif = true;
|
||||
|
||||
@Column(name = "devise_base", length = 3)
|
||||
private String deviseBase;
|
||||
|
||||
@Column(name = "commentaires", columnDefinition = "TEXT")
|
||||
private String commentaires;
|
||||
|
||||
@OneToMany(mappedBy = "grille", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<TarifFretEntity> tarifsFret = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "grille", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<FraisAdditionnelsEntity> fraisAdditionnels = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "grille", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<SurchargeDangereuse> surchargesDangereuses = new ArrayList<>();
|
||||
|
||||
public enum TypeService {
|
||||
IMPORT, EXPORT
|
||||
}
|
||||
|
||||
public enum ModeTransport {
|
||||
MARITIME, AERIEN, ROUTIER, FERROVIAIRE
|
||||
}
|
||||
|
||||
public enum ServiceType {
|
||||
RAPIDE, STANDARD, ECONOMIQUE
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@FieldNameConstants
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Table(name = "surcharges_dangereuses",
|
||||
indexes = {
|
||||
@Index(name = "idx_grille_classe", columnList = "grille_id, classe_adr")
|
||||
})
|
||||
public class SurchargeDangereuse extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "grille_id", nullable = false)
|
||||
private GrilleTarifaireEntity grille;
|
||||
|
||||
@Column(name = "classe_adr", length = 10)
|
||||
private String classeAdr;
|
||||
|
||||
@Column(name = "un_number", length = 10)
|
||||
private String unNumber;
|
||||
|
||||
@Column(name = "surcharge", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal surcharge;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "unite_facturation", nullable = false)
|
||||
private UniteFacturation uniteFacturation;
|
||||
|
||||
@Column(name = "minimum", precision = 10, scale = 2)
|
||||
private BigDecimal minimum;
|
||||
|
||||
public enum UniteFacturation {
|
||||
LS, KG, COLIS
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.dh7789dev.xpeditis.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@FieldNameConstants
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Table(name = "tarifs_fret",
|
||||
indexes = {
|
||||
@Index(name = "idx_grille_poids", columnList = "grille_id, poids_min, poids_max"),
|
||||
@Index(name = "idx_grille_volume", columnList = "grille_id, volume_min, volume_max")
|
||||
})
|
||||
public class TarifFretEntity extends BaseEntity {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "grille_id", nullable = false)
|
||||
private GrilleTarifaireEntity grille;
|
||||
|
||||
@Column(name = "poids_min", precision = 10, scale = 2)
|
||||
private BigDecimal poidsMin;
|
||||
|
||||
@Column(name = "poids_max", precision = 10, scale = 2)
|
||||
private BigDecimal poidsMax;
|
||||
|
||||
@Column(name = "volume_min", precision = 10, scale = 3)
|
||||
private BigDecimal volumeMin;
|
||||
|
||||
@Column(name = "volume_max", precision = 10, scale = 3)
|
||||
private BigDecimal volumeMax;
|
||||
|
||||
@Column(name = "taux_unitaire", nullable = false, precision = 10, scale = 2)
|
||||
private BigDecimal tauxUnitaire;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "unite_facturation", nullable = false)
|
||||
private UniteFacturation uniteFacturation;
|
||||
|
||||
@Column(name = "minimum_facturation", precision = 10, scale = 2)
|
||||
private BigDecimal minimumFacturation;
|
||||
|
||||
public enum UniteFacturation {
|
||||
KG, M3, PALETTE, COLIS, LS
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,271 @@
|
||||
package com.dh7789dev.xpeditis.mapper;
|
||||
|
||||
import com.dh7789dev.xpeditis.dto.app.*;
|
||||
import com.dh7789dev.xpeditis.entity.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class GrilleTarifaireMapper {
|
||||
|
||||
public GrilleTarifaire entityToDto(GrilleTarifaireEntity entity) {
|
||||
if (entity == null) return null;
|
||||
|
||||
GrilleTarifaire dto = new GrilleTarifaire();
|
||||
dto.setId(entity.getId());
|
||||
dto.setNomGrille(entity.getNomGrille());
|
||||
dto.setTransporteur(entity.getTransporteur());
|
||||
dto.setTypeService(mapTypeService(entity.getTypeService()));
|
||||
dto.setOriginePays(entity.getOriginePays());
|
||||
dto.setOrigineVille(entity.getOrigineVille());
|
||||
dto.setOriginePortCode(entity.getOriginePortCode());
|
||||
dto.setDestinationPays(entity.getDestinationPays());
|
||||
dto.setDestinationVille(entity.getDestinationVille());
|
||||
dto.setDestinationPortCode(entity.getDestinationPortCode());
|
||||
dto.setIncoterm(entity.getIncoterm());
|
||||
dto.setModeTransport(mapModeTransport(entity.getModeTransport()));
|
||||
dto.setServiceType(mapServiceType(entity.getServiceType()));
|
||||
dto.setTransitTimeMin(entity.getTransitTimeMin());
|
||||
dto.setTransitTimeMax(entity.getTransitTimeMax());
|
||||
dto.setValiditeDebut(entity.getValiditeDebut());
|
||||
dto.setValiditeFin(entity.getValiditeFin());
|
||||
dto.setDevise(entity.getDevise());
|
||||
dto.setActif(entity.getActif());
|
||||
dto.setDeviseBase(entity.getDeviseBase());
|
||||
dto.setCommentaires(entity.getCommentaires());
|
||||
|
||||
// Mapper les listes associées
|
||||
if (entity.getTarifsFret() != null) {
|
||||
dto.setTarifsFret(entity.getTarifsFret().stream()
|
||||
.map(this::tarifFretEntityToDto)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
if (entity.getFraisAdditionnels() != null) {
|
||||
dto.setFraisAdditionnels(entity.getFraisAdditionnels().stream()
|
||||
.map(this::fraisAdditionnelsEntityToDto)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
if (entity.getSurchargesDangereuses() != null) {
|
||||
dto.setSurchargesDangereuses(entity.getSurchargesDangereuses().stream()
|
||||
.map(this::surchargeDangereueEntityToDto)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public GrilleTarifaireEntity dtoToEntity(GrilleTarifaire dto) {
|
||||
if (dto == null) return null;
|
||||
|
||||
GrilleTarifaireEntity entity = new GrilleTarifaireEntity();
|
||||
entity.setId(dto.getId());
|
||||
entity.setNomGrille(dto.getNomGrille());
|
||||
entity.setTransporteur(dto.getTransporteur());
|
||||
entity.setTypeService(mapTypeServiceToEntity(dto.getTypeService()));
|
||||
entity.setOriginePays(dto.getOriginePays());
|
||||
entity.setOrigineVille(dto.getOrigineVille());
|
||||
entity.setOriginePortCode(dto.getOriginePortCode());
|
||||
entity.setDestinationPays(dto.getDestinationPays());
|
||||
entity.setDestinationVille(dto.getDestinationVille());
|
||||
entity.setDestinationPortCode(dto.getDestinationPortCode());
|
||||
entity.setIncoterm(dto.getIncoterm());
|
||||
entity.setModeTransport(mapModeTransportToEntity(dto.getModeTransport()));
|
||||
entity.setServiceType(mapServiceTypeToEntity(dto.getServiceType()));
|
||||
entity.setTransitTimeMin(dto.getTransitTimeMin());
|
||||
entity.setTransitTimeMax(dto.getTransitTimeMax());
|
||||
entity.setValiditeDebut(dto.getValiditeDebut());
|
||||
entity.setValiditeFin(dto.getValiditeFin());
|
||||
entity.setDevise(dto.getDevise());
|
||||
entity.setActif(dto.getActif());
|
||||
entity.setDeviseBase(dto.getDeviseBase());
|
||||
entity.setCommentaires(dto.getCommentaires());
|
||||
|
||||
// Mapper les listes associées
|
||||
if (dto.getTarifsFret() != null) {
|
||||
List<TarifFretEntity> tarifsFret = dto.getTarifsFret().stream()
|
||||
.map(this::tarifFretDtoToEntity)
|
||||
.collect(Collectors.toList());
|
||||
tarifsFret.forEach(tarif -> tarif.setGrille(entity));
|
||||
entity.setTarifsFret(tarifsFret);
|
||||
}
|
||||
|
||||
if (dto.getFraisAdditionnels() != null) {
|
||||
List<FraisAdditionnelsEntity> fraisAdditionnels = dto.getFraisAdditionnels().stream()
|
||||
.map(this::fraisAdditionnelsDtoToEntity)
|
||||
.collect(Collectors.toList());
|
||||
fraisAdditionnels.forEach(frais -> frais.setGrille(entity));
|
||||
entity.setFraisAdditionnels(fraisAdditionnels);
|
||||
}
|
||||
|
||||
if (dto.getSurchargesDangereuses() != null) {
|
||||
List<com.dh7789dev.xpeditis.entity.SurchargeDangereuse> surchargesDangereuses = dto.getSurchargesDangereuses().stream()
|
||||
.map(this::surchargeDangereueDtoToEntity)
|
||||
.collect(Collectors.toList());
|
||||
surchargesDangereuses.forEach(surcharge -> surcharge.setGrille(entity));
|
||||
entity.setSurchargesDangereuses(surchargesDangereuses);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private TarifFret tarifFretEntityToDto(TarifFretEntity entity) {
|
||||
if (entity == null) return null;
|
||||
|
||||
TarifFret dto = new TarifFret();
|
||||
dto.setId(entity.getId());
|
||||
dto.setGrilleId(entity.getGrille() != null ? entity.getGrille().getId() : null);
|
||||
dto.setPoidsMin(entity.getPoidsMin());
|
||||
dto.setPoidsMax(entity.getPoidsMax());
|
||||
dto.setVolumeMin(entity.getVolumeMin());
|
||||
dto.setVolumeMax(entity.getVolumeMax());
|
||||
dto.setTauxUnitaire(entity.getTauxUnitaire());
|
||||
dto.setUniteFacturation(mapUniteFacturationTarifFret(entity.getUniteFacturation()));
|
||||
dto.setMinimumFacturation(entity.getMinimumFacturation());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private TarifFretEntity tarifFretDtoToEntity(TarifFret dto) {
|
||||
if (dto == null) return null;
|
||||
|
||||
TarifFretEntity entity = new TarifFretEntity();
|
||||
entity.setId(dto.getId());
|
||||
entity.setPoidsMin(dto.getPoidsMin());
|
||||
entity.setPoidsMax(dto.getPoidsMax());
|
||||
entity.setVolumeMin(dto.getVolumeMin());
|
||||
entity.setVolumeMax(dto.getVolumeMax());
|
||||
entity.setTauxUnitaire(dto.getTauxUnitaire());
|
||||
entity.setUniteFacturation(mapUniteFacturationTarifFretToEntity(dto.getUniteFacturation()));
|
||||
entity.setMinimumFacturation(dto.getMinimumFacturation());
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private FraisAdditionnels fraisAdditionnelsEntityToDto(FraisAdditionnelsEntity entity) {
|
||||
if (entity == null) return null;
|
||||
|
||||
FraisAdditionnels dto = new FraisAdditionnels();
|
||||
dto.setId(entity.getId());
|
||||
dto.setGrilleId(entity.getGrille() != null ? entity.getGrille().getId() : null);
|
||||
dto.setTypeFrais(entity.getTypeFrais());
|
||||
dto.setDescription(entity.getDescription());
|
||||
dto.setMontant(entity.getMontant());
|
||||
dto.setUniteFacturation(mapUniteFacturationFrais(entity.getUniteFacturation()));
|
||||
dto.setMontantMinimum(entity.getMontantMinimum());
|
||||
dto.setObligatoire(entity.getObligatoire());
|
||||
dto.setApplicableMarchandiseDangereuse(entity.getApplicableMarchandiseDangereuse());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private FraisAdditionnelsEntity fraisAdditionnelsDtoToEntity(FraisAdditionnels dto) {
|
||||
if (dto == null) return null;
|
||||
|
||||
FraisAdditionnelsEntity entity = new FraisAdditionnelsEntity();
|
||||
entity.setId(dto.getId());
|
||||
entity.setTypeFrais(dto.getTypeFrais());
|
||||
entity.setDescription(dto.getDescription());
|
||||
entity.setMontant(dto.getMontant());
|
||||
entity.setUniteFacturation(mapUniteFacturationFraisToEntity(dto.getUniteFacturation()));
|
||||
entity.setMontantMinimum(dto.getMontantMinimum());
|
||||
entity.setObligatoire(dto.getObligatoire());
|
||||
entity.setApplicableMarchandiseDangereuse(dto.getApplicableMarchandiseDangereuse());
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private com.dh7789dev.xpeditis.dto.app.SurchargeDangereuse surchargeDangereueEntityToDto(com.dh7789dev.xpeditis.entity.SurchargeDangereuse entity) {
|
||||
if (entity == null) return null;
|
||||
|
||||
com.dh7789dev.xpeditis.dto.app.SurchargeDangereuse dto = new com.dh7789dev.xpeditis.dto.app.SurchargeDangereuse();
|
||||
dto.setId(entity.getId());
|
||||
dto.setGrilleId(entity.getGrille() != null ? entity.getGrille().getId() : null);
|
||||
dto.setClasseAdr(entity.getClasseAdr());
|
||||
dto.setUnNumber(entity.getUnNumber());
|
||||
dto.setSurcharge(entity.getSurcharge());
|
||||
dto.setUniteFacturation(mapUniteFacturationSurcharge(entity.getUniteFacturation()));
|
||||
dto.setMinimum(entity.getMinimum());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private com.dh7789dev.xpeditis.entity.SurchargeDangereuse surchargeDangereueDtoToEntity(com.dh7789dev.xpeditis.dto.app.SurchargeDangereuse dto) {
|
||||
if (dto == null) return null;
|
||||
|
||||
com.dh7789dev.xpeditis.entity.SurchargeDangereuse entity = new com.dh7789dev.xpeditis.entity.SurchargeDangereuse();
|
||||
entity.setId(dto.getId());
|
||||
entity.setClasseAdr(dto.getClasseAdr());
|
||||
entity.setUnNumber(dto.getUnNumber());
|
||||
entity.setSurcharge(dto.getSurcharge());
|
||||
entity.setUniteFacturation(mapUniteFacturationSurchargeToEntity(dto.getUniteFacturation()));
|
||||
entity.setMinimum(dto.getMinimum());
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
// Méthodes de mapping des enums
|
||||
private GrilleTarifaire.TypeService mapTypeService(GrilleTarifaireEntity.TypeService typeService) {
|
||||
if (typeService == null) return null;
|
||||
return GrilleTarifaire.TypeService.valueOf(typeService.name());
|
||||
}
|
||||
|
||||
private GrilleTarifaireEntity.TypeService mapTypeServiceToEntity(GrilleTarifaire.TypeService typeService) {
|
||||
if (typeService == null) return null;
|
||||
return GrilleTarifaireEntity.TypeService.valueOf(typeService.name());
|
||||
}
|
||||
|
||||
private GrilleTarifaire.ModeTransport mapModeTransport(GrilleTarifaireEntity.ModeTransport modeTransport) {
|
||||
if (modeTransport == null) return null;
|
||||
return GrilleTarifaire.ModeTransport.valueOf(modeTransport.name());
|
||||
}
|
||||
|
||||
private GrilleTarifaireEntity.ModeTransport mapModeTransportToEntity(GrilleTarifaire.ModeTransport modeTransport) {
|
||||
if (modeTransport == null) return null;
|
||||
return GrilleTarifaireEntity.ModeTransport.valueOf(modeTransport.name());
|
||||
}
|
||||
|
||||
private GrilleTarifaire.ServiceType mapServiceType(GrilleTarifaireEntity.ServiceType serviceType) {
|
||||
if (serviceType == null) return null;
|
||||
return GrilleTarifaire.ServiceType.valueOf(serviceType.name());
|
||||
}
|
||||
|
||||
private GrilleTarifaireEntity.ServiceType mapServiceTypeToEntity(GrilleTarifaire.ServiceType serviceType) {
|
||||
if (serviceType == null) return null;
|
||||
return GrilleTarifaireEntity.ServiceType.valueOf(serviceType.name());
|
||||
}
|
||||
|
||||
private TarifFret.UniteFacturation mapUniteFacturationTarifFret(TarifFretEntity.UniteFacturation uniteFacturation) {
|
||||
if (uniteFacturation == null) return null;
|
||||
return TarifFret.UniteFacturation.valueOf(uniteFacturation.name());
|
||||
}
|
||||
|
||||
private TarifFretEntity.UniteFacturation mapUniteFacturationTarifFretToEntity(TarifFret.UniteFacturation uniteFacturation) {
|
||||
if (uniteFacturation == null) return null;
|
||||
return TarifFretEntity.UniteFacturation.valueOf(uniteFacturation.name());
|
||||
}
|
||||
|
||||
private FraisAdditionnels.UniteFacturation mapUniteFacturationFrais(FraisAdditionnelsEntity.UniteFacturation uniteFacturation) {
|
||||
if (uniteFacturation == null) return null;
|
||||
return FraisAdditionnels.UniteFacturation.valueOf(uniteFacturation.name());
|
||||
}
|
||||
|
||||
private FraisAdditionnelsEntity.UniteFacturation mapUniteFacturationFraisToEntity(FraisAdditionnels.UniteFacturation uniteFacturation) {
|
||||
if (uniteFacturation == null) return null;
|
||||
return FraisAdditionnelsEntity.UniteFacturation.valueOf(uniteFacturation.name());
|
||||
}
|
||||
|
||||
private com.dh7789dev.xpeditis.dto.app.SurchargeDangereuse.UniteFacturation mapUniteFacturationSurcharge(com.dh7789dev.xpeditis.entity.SurchargeDangereuse.UniteFacturation uniteFacturation) {
|
||||
if (uniteFacturation == null) return null;
|
||||
return com.dh7789dev.xpeditis.dto.app.SurchargeDangereuse.UniteFacturation.valueOf(uniteFacturation.name());
|
||||
}
|
||||
|
||||
private com.dh7789dev.xpeditis.entity.SurchargeDangereuse.UniteFacturation mapUniteFacturationSurchargeToEntity(com.dh7789dev.xpeditis.dto.app.SurchargeDangereuse.UniteFacturation uniteFacturation) {
|
||||
if (uniteFacturation == null) return null;
|
||||
return com.dh7789dev.xpeditis.entity.SurchargeDangereuse.UniteFacturation.valueOf(uniteFacturation.name());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.dh7789dev.xpeditis.repository;
|
||||
|
||||
import com.dh7789dev.xpeditis.entity.FraisAdditionnelsEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface FraisAdditionnelsJpaRepository extends JpaRepository<FraisAdditionnelsEntity, Long> {
|
||||
|
||||
/**
|
||||
* Trouve tous les frais additionnels pour une grille donnée
|
||||
*/
|
||||
List<FraisAdditionnelsEntity> findByGrilleIdOrderByTypeFrais(Long grilleId);
|
||||
|
||||
/**
|
||||
* Trouve les frais obligatoires pour une grille
|
||||
*/
|
||||
List<FraisAdditionnelsEntity> findByGrilleIdAndObligatoireTrue(Long grilleId);
|
||||
|
||||
/**
|
||||
* Trouve les frais par type pour une grille
|
||||
*/
|
||||
List<FraisAdditionnelsEntity> findByGrilleIdAndTypeFrais(Long grilleId, String typeFrais);
|
||||
|
||||
/**
|
||||
* Trouve les frais applicables aux marchandises dangereuses
|
||||
*/
|
||||
@Query("SELECT f FROM FraisAdditionnelsEntity f " +
|
||||
"WHERE f.grille.id = :grilleId " +
|
||||
"AND (f.applicableMarchandiseDangereuse = true OR f.obligatoire = true)")
|
||||
List<FraisAdditionnelsEntity> findFraisApplicablesMarchandiseDangereuse(@Param("grilleId") Long grilleId);
|
||||
|
||||
/**
|
||||
* Supprime tous les frais additionnels d'une grille
|
||||
*/
|
||||
void deleteByGrilleId(Long grilleId);
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package com.dh7789dev.xpeditis.repository;
|
||||
|
||||
import com.dh7789dev.xpeditis.entity.GrilleTarifaireEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface GrilleTarifaireJpaRepository extends JpaRepository<GrilleTarifaireEntity, Long> {
|
||||
|
||||
/**
|
||||
* Trouve toutes les grilles tarifaires valides pour une route et période donnée
|
||||
*/
|
||||
@Query("SELECT g FROM GrilleTarifaireEntity g " +
|
||||
"WHERE g.typeService = :typeService " +
|
||||
"AND g.originePays = :originePays " +
|
||||
"AND g.destinationPays = :destinationPays " +
|
||||
"AND g.validiteDebut <= :dateValidite " +
|
||||
"AND g.validiteFin >= :dateValidite " +
|
||||
"ORDER BY g.serviceType, g.origineVille NULLS LAST, g.destinationVille NULLS LAST")
|
||||
List<GrilleTarifaireEntity> findGrillesApplicables(
|
||||
@Param("typeService") GrilleTarifaireEntity.TypeService typeService,
|
||||
@Param("originePays") String originePays,
|
||||
@Param("destinationPays") String destinationPays,
|
||||
@Param("dateValidite") LocalDate dateValidite
|
||||
);
|
||||
|
||||
/**
|
||||
* Trouve toutes les grilles tarifaires d'un type de service spécifique
|
||||
*/
|
||||
@Query("SELECT g FROM GrilleTarifaireEntity g " +
|
||||
"WHERE g.typeService = :typeService " +
|
||||
"AND g.serviceType = :serviceType " +
|
||||
"AND g.originePays = :originePays " +
|
||||
"AND g.destinationPays = :destinationPays " +
|
||||
"AND g.validiteDebut <= :dateValidite " +
|
||||
"AND g.validiteFin >= :dateValidite " +
|
||||
"ORDER BY g.origineVille NULLS LAST, g.destinationVille NULLS LAST")
|
||||
List<GrilleTarifaireEntity> findByServiceTypeAndRoute(
|
||||
@Param("typeService") GrilleTarifaireEntity.TypeService typeService,
|
||||
@Param("serviceType") GrilleTarifaireEntity.ServiceType serviceType,
|
||||
@Param("originePays") String originePays,
|
||||
@Param("destinationPays") String destinationPays,
|
||||
@Param("dateValidite") LocalDate dateValidite
|
||||
);
|
||||
|
||||
/**
|
||||
* Trouve les grilles par transporteur
|
||||
*/
|
||||
List<GrilleTarifaireEntity> findByTransporteurAndValiditeDebutLessThanEqualAndValiditeFinGreaterThanEqual(
|
||||
String transporteur,
|
||||
LocalDate dateDebut,
|
||||
LocalDate dateFin
|
||||
);
|
||||
|
||||
/**
|
||||
* Trouve les grilles par route
|
||||
*/
|
||||
List<GrilleTarifaireEntity> findByOriginePaysAndDestinationPaysAndValiditeDebutLessThanEqualAndValiditeFinGreaterThanEqual(
|
||||
String originePays,
|
||||
String destinationPays,
|
||||
LocalDate dateDebut,
|
||||
LocalDate dateFin
|
||||
);
|
||||
|
||||
/**
|
||||
* Compte les grilles actives pour un transporteur
|
||||
*/
|
||||
@Query("SELECT COUNT(g) FROM GrilleTarifaireEntity g " +
|
||||
"WHERE g.transporteur = :transporteur " +
|
||||
"AND g.validiteDebut <= :date " +
|
||||
"AND g.validiteFin >= :date")
|
||||
Long countActiveGridsByTransporteur(@Param("transporteur") String transporteur, @Param("date") LocalDate date);
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.dh7789dev.xpeditis.repository;
|
||||
|
||||
import com.dh7789dev.xpeditis.entity.SurchargeDangereuse;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SurchargeDangereuseJpaRepository extends JpaRepository<SurchargeDangereuse, Long> {
|
||||
|
||||
/**
|
||||
* Trouve toutes les surcharges pour une grille donnée
|
||||
*/
|
||||
List<SurchargeDangereuse> findByGrilleIdOrderByClasseAdr(Long grilleId);
|
||||
|
||||
/**
|
||||
* Trouve une surcharge par classe ADR pour une grille
|
||||
*/
|
||||
Optional<SurchargeDangereuse> findByGrilleIdAndClasseAdr(Long grilleId, String classeAdr);
|
||||
|
||||
/**
|
||||
* Trouve une surcharge par numéro UN pour une grille
|
||||
*/
|
||||
Optional<SurchargeDangereuse> findByGrilleIdAndUnNumber(Long grilleId, String unNumber);
|
||||
|
||||
/**
|
||||
* Trouve les surcharges par classe ADR
|
||||
*/
|
||||
List<SurchargeDangereuse> findByClasseAdr(String classeAdr);
|
||||
|
||||
/**
|
||||
* Supprime toutes les surcharges d'une grille
|
||||
*/
|
||||
void deleteByGrilleId(Long grilleId);
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package com.dh7789dev.xpeditis.repository;
|
||||
|
||||
import com.dh7789dev.xpeditis.entity.TarifFretEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface TarifFretJpaRepository extends JpaRepository<TarifFretEntity, Long> {
|
||||
|
||||
/**
|
||||
* Trouve tous les tarifs de fret pour une grille donnée
|
||||
*/
|
||||
List<TarifFretEntity> findByGrilleIdOrderByPoidsMinAsc(Long grilleId);
|
||||
|
||||
/**
|
||||
* Trouve le tarif applicable selon le poids
|
||||
*/
|
||||
@Query("SELECT t FROM TarifFretEntity t " +
|
||||
"WHERE t.grille.id = :grilleId " +
|
||||
"AND (t.poidsMin IS NULL OR t.poidsMin <= :poids) " +
|
||||
"AND (t.poidsMax IS NULL OR t.poidsMax >= :poids) " +
|
||||
"ORDER BY t.poidsMin ASC")
|
||||
Optional<TarifFretEntity> findTarifApplicableByPoids(
|
||||
@Param("grilleId") Long grilleId,
|
||||
@Param("poids") BigDecimal poids
|
||||
);
|
||||
|
||||
/**
|
||||
* Trouve le tarif applicable selon le volume
|
||||
*/
|
||||
@Query("SELECT t FROM TarifFretEntity t " +
|
||||
"WHERE t.grille.id = :grilleId " +
|
||||
"AND (t.volumeMin IS NULL OR t.volumeMin <= :volume) " +
|
||||
"AND (t.volumeMax IS NULL OR t.volumeMax >= :volume) " +
|
||||
"ORDER BY t.volumeMin ASC")
|
||||
Optional<TarifFretEntity> findTarifApplicableByVolume(
|
||||
@Param("grilleId") Long grilleId,
|
||||
@Param("volume") BigDecimal volume
|
||||
);
|
||||
|
||||
/**
|
||||
* Supprime tous les tarifs d'une grille
|
||||
*/
|
||||
void deleteByGrilleId(Long grilleId);
|
||||
}
|
||||
@ -0,0 +1,201 @@
|
||||
-- ================================
|
||||
-- Migration V3.1: Données d'exemple pour les Grilles Tarifaires
|
||||
-- Basé sur la grille LESCHACO Fos Sur Mer
|
||||
-- Créé le: 2024-09-12
|
||||
-- ================================
|
||||
|
||||
-- ================================
|
||||
-- 1. Grille LESCHACO - France vers Chine (Shanghai)
|
||||
-- ================================
|
||||
|
||||
-- Grille Standard
|
||||
INSERT INTO grilles_tarifaires (
|
||||
nom_grille, transporteur, type_service, origine_pays, origine_ville, origine_port_code,
|
||||
destination_pays, destination_ville, destination_port_code, incoterm, mode_transport,
|
||||
service_type, transit_time_min, transit_time_max, validite_debut, validite_fin, devise
|
||||
) VALUES (
|
||||
'LESCHACO FOS-SHA Standard 2024', 'LESCHACO', 'EXPORT', 'FRA', 'Fos Sur Mer', 'FOS',
|
||||
'CHN', 'Shanghai', 'SHA', 'FOB', 'MARITIME', 'STANDARD', 25, 30, '2024-01-01', '2024-12-31', 'EUR'
|
||||
);
|
||||
|
||||
-- Grille Rapide
|
||||
INSERT INTO grilles_tarifaires (
|
||||
nom_grille, transporteur, type_service, origine_pays, origine_ville, origine_port_code,
|
||||
destination_pays, destination_ville, destination_port_code, incoterm, mode_transport,
|
||||
service_type, transit_time_min, transit_time_max, validite_debut, validite_fin, devise
|
||||
) VALUES (
|
||||
'LESCHACO FOS-SHA Express 2024', 'LESCHACO', 'EXPORT', 'FRA', 'Fos Sur Mer', 'FOS',
|
||||
'CHN', 'Shanghai', 'SHA', 'FOB', 'MARITIME', 'RAPIDE', 18, 22, '2024-01-01', '2024-12-31', 'EUR'
|
||||
);
|
||||
|
||||
-- Grille Économique
|
||||
INSERT INTO grilles_tarifaires (
|
||||
nom_grille, transporteur, type_service, origine_pays, origine_ville, origine_port_code,
|
||||
destination_pays, destination_ville, destination_port_code, incoterm, mode_transport,
|
||||
service_type, transit_time_min, transit_time_max, validite_debut, validite_fin, devise
|
||||
) VALUES (
|
||||
'LESCHACO FOS-SHA Eco 2024', 'LESCHACO', 'EXPORT', 'FRA', 'Fos Sur Mer', 'FOS',
|
||||
'CHN', 'Shanghai', 'SHA', 'FOB', 'MARITIME', 'ECONOMIQUE', 35, 40, '2024-01-01', '2024-12-31', 'EUR'
|
||||
);
|
||||
|
||||
-- ================================
|
||||
-- 2. Tarifs de fret pour les 3 grilles
|
||||
-- ================================
|
||||
|
||||
-- Tarifs pour grille Standard (ID 1)
|
||||
INSERT INTO tarifs_fret (grille_id, poids_min, poids_max, taux_unitaire, unite_facturation, minimum_facturation) VALUES
|
||||
(1, 0, 100, 85.00, 'KG', 150.00),
|
||||
(1, 100, 500, 75.00, 'KG', null),
|
||||
(1, 500, 1000, 65.00, 'KG', null),
|
||||
(1, 1000, 5000, 55.00, 'KG', null),
|
||||
(1, 5000, null, 45.00, 'KG', null);
|
||||
|
||||
-- Tarifs pour grille Rapide (ID 2) - mêmes tarifs de base
|
||||
INSERT INTO tarifs_fret (grille_id, poids_min, poids_max, taux_unitaire, unite_facturation, minimum_facturation) VALUES
|
||||
(2, 0, 100, 85.00, 'KG', 150.00),
|
||||
(2, 100, 500, 75.00, 'KG', null),
|
||||
(2, 500, 1000, 65.00, 'KG', null),
|
||||
(2, 1000, 5000, 55.00, 'KG', null),
|
||||
(2, 5000, null, 45.00, 'KG', null);
|
||||
|
||||
-- Tarifs pour grille Économique (ID 3) - mêmes tarifs de base
|
||||
INSERT INTO tarifs_fret (grille_id, poids_min, poids_max, taux_unitaire, unite_facturation, minimum_facturation) VALUES
|
||||
(3, 0, 100, 85.00, 'KG', 150.00),
|
||||
(3, 100, 500, 75.00, 'KG', null),
|
||||
(3, 500, 1000, 65.00, 'KG', null),
|
||||
(3, 1000, 5000, 55.00, 'KG', null),
|
||||
(3, 5000, null, 45.00, 'KG', null);
|
||||
|
||||
-- ================================
|
||||
-- 3. Frais additionnels obligatoires
|
||||
-- ================================
|
||||
|
||||
-- Frais obligatoires pour la grille Standard
|
||||
INSERT INTO frais_additionnels (grille_id, type_frais, description, montant, unite_facturation, obligatoire) VALUES
|
||||
(1, 'DOCUMENTATION', 'Frais de documentation export', 32.00, 'LS', TRUE),
|
||||
(1, 'ISPS', 'International Ship and Port Security', 15.00, 'LS', TRUE),
|
||||
(1, 'MANUTENTION', 'Frais de manutention portuaire', 85.50, 'LS', TRUE);
|
||||
|
||||
-- Frais obligatoires pour la grille Rapide
|
||||
INSERT INTO frais_additionnels (grille_id, type_frais, description, montant, unite_facturation, obligatoire) VALUES
|
||||
(2, 'DOCUMENTATION', 'Frais de documentation export', 32.00, 'LS', TRUE),
|
||||
(2, 'ISPS', 'International Ship and Port Security', 15.00, 'LS', TRUE),
|
||||
(2, 'MANUTENTION', 'Frais de manutention portuaire', 85.50, 'LS', TRUE);
|
||||
|
||||
-- Frais obligatoires pour la grille Économique
|
||||
INSERT INTO frais_additionnels (grille_id, type_frais, description, montant, unite_facturation, obligatoire) VALUES
|
||||
(3, 'DOCUMENTATION', 'Frais de documentation export', 32.00, 'LS', TRUE),
|
||||
(3, 'ISPS', 'International Ship and Port Security', 15.00, 'LS', TRUE),
|
||||
(3, 'MANUTENTION', 'Frais de manutention portuaire', 85.50, 'LS', TRUE);
|
||||
|
||||
-- ================================
|
||||
-- 4. Frais optionnels
|
||||
-- ================================
|
||||
|
||||
-- Frais optionnels pour toutes les grilles
|
||||
INSERT INTO frais_additionnels (grille_id, type_frais, description, montant, unite_facturation, obligatoire) VALUES
|
||||
-- Grille Standard
|
||||
(1, 'ASSURANCE', 'Assurance transport maritime', 45.00, 'LS', FALSE),
|
||||
(1, 'HAYON', 'Service de hayon pour livraison', 35.00, 'LS', FALSE),
|
||||
(1, 'SANGLES', 'Sanglage et arrimage', 25.00, 'LS', FALSE),
|
||||
(1, 'COUVERTURE_THERMIQUE', 'Protection thermique', 40.00, 'LS', FALSE),
|
||||
|
||||
-- Grille Rapide
|
||||
(2, 'ASSURANCE', 'Assurance transport maritime', 45.00, 'LS', FALSE),
|
||||
(2, 'HAYON', 'Service de hayon pour livraison', 35.00, 'LS', FALSE),
|
||||
(2, 'SANGLES', 'Sanglage et arrimage', 25.00, 'LS', FALSE),
|
||||
(2, 'COUVERTURE_THERMIQUE', 'Protection thermique', 40.00, 'LS', FALSE),
|
||||
|
||||
-- Grille Économique
|
||||
(3, 'ASSURANCE', 'Assurance transport maritime', 45.00, 'LS', FALSE),
|
||||
(3, 'HAYON', 'Service de hayon pour livraison', 35.00, 'LS', FALSE),
|
||||
(3, 'SANGLES', 'Sanglage et arrimage', 25.00, 'LS', FALSE),
|
||||
(3, 'COUVERTURE_THERMIQUE', 'Protection thermique', 40.00, 'LS', FALSE);
|
||||
|
||||
-- ================================
|
||||
-- 5. Surcharges marchandises dangereuses
|
||||
-- ================================
|
||||
|
||||
-- Surcharges pour matières dangereuses (toutes les grilles)
|
||||
INSERT INTO surcharges_dangereuses (grille_id, classe_adr, surcharge, unite_facturation, minimum) VALUES
|
||||
-- Grille Standard
|
||||
(1, '1', 150.00, 'LS', 100.00), -- Matières explosives
|
||||
(1, '2.1', 75.00, 'LS', 50.00), -- Gaz inflammables
|
||||
(1, '3', 85.00, 'LS', 60.00), -- Liquides inflammables
|
||||
(1, '4.1', 70.00, 'LS', 50.00), -- Matières solides inflammables
|
||||
(1, '5.1', 90.00, 'LS', 65.00), -- Matières comburantes
|
||||
(1, '6.1', 120.00, 'LS', 80.00), -- Matières toxiques
|
||||
(1, '8', 95.00, 'LS', 70.00), -- Matières corrosives
|
||||
(1, '9', 55.00, 'LS', 40.00), -- Matières dangereuses diverses
|
||||
|
||||
-- Grille Rapide (surcharges identiques)
|
||||
(2, '1', 150.00, 'LS', 100.00),
|
||||
(2, '2.1', 75.00, 'LS', 50.00),
|
||||
(2, '3', 85.00, 'LS', 60.00),
|
||||
(2, '4.1', 70.00, 'LS', 50.00),
|
||||
(2, '5.1', 90.00, 'LS', 65.00),
|
||||
(2, '6.1', 120.00, 'LS', 80.00),
|
||||
(2, '8', 95.00, 'LS', 70.00),
|
||||
(2, '9', 55.00, 'LS', 40.00),
|
||||
|
||||
-- Grille Économique (surcharges identiques)
|
||||
(3, '1', 150.00, 'LS', 100.00),
|
||||
(3, '2.1', 75.00, 'LS', 50.00),
|
||||
(3, '3', 85.00, 'LS', 60.00),
|
||||
(3, '4.1', 70.00, 'LS', 50.00),
|
||||
(3, '5.1', 90.00, 'LS', 65.00),
|
||||
(3, '6.1', 120.00, 'LS', 80.00),
|
||||
(3, '8', 95.00, 'LS', 70.00),
|
||||
(3, '9', 55.00, 'LS', 40.00);
|
||||
|
||||
-- ================================
|
||||
-- 6. Grilles additionnelles - Hong Kong et Singapour
|
||||
-- ================================
|
||||
|
||||
-- France vers Hong Kong
|
||||
INSERT INTO grilles_tarifaires (
|
||||
nom_grille, transporteur, type_service, origine_pays, origine_ville, origine_port_code,
|
||||
destination_pays, destination_ville, destination_port_code, incoterm, mode_transport,
|
||||
service_type, transit_time_min, transit_time_max, validite_debut, validite_fin, devise
|
||||
) VALUES
|
||||
('LESCHACO FOS-HKG Standard 2024', 'LESCHACO', 'EXPORT', 'FRA', 'Fos Sur Mer', 'FOS',
|
||||
'HKG', 'Hong Kong', 'HKG', 'FOB', 'MARITIME', 'STANDARD', 28, 33, '2024-01-01', '2024-12-31', 'EUR');
|
||||
|
||||
-- France vers Singapour
|
||||
INSERT INTO grilles_tarifaires (
|
||||
nom_grille, transporteur, type_service, origine_pays, origine_ville, origine_port_code,
|
||||
destination_pays, destination_ville, destination_port_code, incoterm, mode_transport,
|
||||
service_type, transit_time_min, transit_time_max, validite_debut, validite_fin, devise
|
||||
) VALUES
|
||||
('LESCHACO FOS-SIN Standard 2024', 'LESCHACO', 'EXPORT', 'FRA', 'Fos Sur Mer', 'FOS',
|
||||
'SGP', 'Singapore', 'SIN', 'FOB', 'MARITIME', 'STANDARD', 26, 31, '2024-01-01', '2024-12-31', 'EUR');
|
||||
|
||||
-- Tarifs pour Hong Kong (grille ID 4) - légèrement plus chers
|
||||
INSERT INTO tarifs_fret (grille_id, poids_min, poids_max, taux_unitaire, unite_facturation, minimum_facturation) VALUES
|
||||
(4, 0, 100, 90.00, 'KG', 160.00),
|
||||
(4, 100, 500, 80.00, 'KG', null),
|
||||
(4, 500, 1000, 70.00, 'KG', null),
|
||||
(4, 1000, 5000, 60.00, 'KG', null),
|
||||
(4, 5000, null, 50.00, 'KG', null);
|
||||
|
||||
-- Tarifs pour Singapour (grille ID 5) - prix similaires à Shanghai
|
||||
INSERT INTO tarifs_fret (grille_id, poids_min, poids_max, taux_unitaire, unite_facturation, minimum_facturation) VALUES
|
||||
(5, 0, 100, 87.00, 'KG', 155.00),
|
||||
(5, 100, 500, 77.00, 'KG', null),
|
||||
(5, 500, 1000, 67.00, 'KG', null),
|
||||
(5, 1000, 5000, 57.00, 'KG', null),
|
||||
(5, 5000, null, 47.00, 'KG', null);
|
||||
|
||||
-- ================================
|
||||
-- Commentaire sur les données d'exemple
|
||||
-- ================================
|
||||
|
||||
-- Ces données d'exemple permettent de tester:
|
||||
-- 1. Le calcul des 3 offres (Rapide, Standard, Économique)
|
||||
-- 2. Les différents transporteurs et routes
|
||||
-- 3. Les frais obligatoires et optionnels
|
||||
-- 4. Les surcharges pour marchandises dangereuses
|
||||
-- 5. Les barèmes de prix progressifs selon le poids
|
||||
|
||||
-- Les prix sont basés sur les tarifs réels du transport maritime LCL
|
||||
-- France-Asie et peuvent être ajustés selon les besoins.
|
||||
@ -0,0 +1,129 @@
|
||||
-- ================================
|
||||
-- Migration V3: Système de Grilles Tarifaires pour Devis Transport
|
||||
-- Créé le: 2024-09-12
|
||||
-- Auteur: Claude Code
|
||||
-- ================================
|
||||
|
||||
-- Table principale des grilles tarifaires
|
||||
CREATE TABLE grilles_tarifaires (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
nom_grille VARCHAR(100) NOT NULL COMMENT 'Nom descriptif de la grille tarifaire',
|
||||
transporteur VARCHAR(50) NOT NULL COMMENT 'Nom du transporteur (LESCHACO, MAERSK, etc.)',
|
||||
type_service ENUM('IMPORT', 'EXPORT') NOT NULL COMMENT 'Type de service transport',
|
||||
origine_pays CHAR(3) NOT NULL COMMENT 'Code pays origine (ISO 3166-1 alpha-3)',
|
||||
origine_ville VARCHAR(100) COMMENT 'Ville d''origine (optionnel)',
|
||||
origine_port_code VARCHAR(10) COMMENT 'Code du port d''origine',
|
||||
destination_pays CHAR(3) NOT NULL COMMENT 'Code pays destination (ISO 3166-1 alpha-3)',
|
||||
destination_ville VARCHAR(100) COMMENT 'Ville de destination (optionnel)',
|
||||
destination_port_code VARCHAR(10) COMMENT 'Code du port de destination',
|
||||
incoterm VARCHAR(10) COMMENT 'Incoterm applicable (EXW, FOB, CIF, etc.)',
|
||||
mode_transport ENUM('MARITIME', 'AERIEN', 'ROUTIER', 'FERROVIAIRE') COMMENT 'Mode de transport',
|
||||
service_type ENUM('RAPIDE', 'STANDARD', 'ECONOMIQUE') COMMENT 'Type de service (pour les 3 offres)',
|
||||
transit_time_min INT COMMENT 'Temps de transit minimum en jours',
|
||||
transit_time_max INT COMMENT 'Temps de transit maximum en jours',
|
||||
validite_debut DATE NOT NULL COMMENT 'Date de début de validité',
|
||||
validite_fin DATE NOT NULL COMMENT 'Date de fin de validité',
|
||||
devise CHAR(3) DEFAULT 'EUR' COMMENT 'Devise des tarifs (ISO 4217)',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- Contraintes
|
||||
CONSTRAINT chk_validite_dates CHECK (validite_debut <= validite_fin),
|
||||
CONSTRAINT chk_transit_times CHECK (transit_time_min IS NULL OR transit_time_max IS NULL OR transit_time_min <= transit_time_max),
|
||||
|
||||
-- Index pour les recherches fréquentes
|
||||
INDEX idx_route_service (type_service, origine_pays, destination_pays, validite_debut, validite_fin),
|
||||
INDEX idx_transporteur_validite (transporteur, validite_debut, validite_fin),
|
||||
INDEX idx_service_type (service_type),
|
||||
INDEX idx_ville_origine (origine_ville),
|
||||
INDEX idx_ville_destination (destination_ville)
|
||||
);
|
||||
|
||||
-- Table des tarifs de fret (barème de prix selon poids/volume)
|
||||
CREATE TABLE tarifs_fret (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
grille_id BIGINT NOT NULL COMMENT 'Référence vers la grille tarifaire',
|
||||
poids_min DECIMAL(10,2) COMMENT 'Poids minimum en kg pour ce tarif',
|
||||
poids_max DECIMAL(10,2) COMMENT 'Poids maximum en kg pour ce tarif',
|
||||
volume_min DECIMAL(10,3) COMMENT 'Volume minimum en m³ pour ce tarif',
|
||||
volume_max DECIMAL(10,3) COMMENT 'Volume maximum en m³ pour ce tarif',
|
||||
taux_unitaire DECIMAL(10,2) NOT NULL COMMENT 'Taux unitaire selon l''unité de facturation',
|
||||
unite_facturation ENUM('KG', 'M3', 'PALETTE', 'COLIS', 'LS') NOT NULL COMMENT 'Unité de facturation',
|
||||
minimum_facturation DECIMAL(10,2) COMMENT 'Montant minimum de facturation',
|
||||
|
||||
-- Clé étrangère
|
||||
FOREIGN KEY (grille_id) REFERENCES grilles_tarifaires(id) ON DELETE CASCADE,
|
||||
|
||||
-- Index pour optimiser les recherches de tarifs
|
||||
INDEX idx_grille_poids (grille_id, poids_min, poids_max),
|
||||
INDEX idx_grille_volume (grille_id, volume_min, volume_max),
|
||||
INDEX idx_unite_facturation (unite_facturation)
|
||||
);
|
||||
|
||||
-- Table des frais additionnels (frais fixes, services optionnels)
|
||||
CREATE TABLE frais_additionnels (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
grille_id BIGINT NOT NULL COMMENT 'Référence vers la grille tarifaire',
|
||||
type_frais VARCHAR(50) NOT NULL COMMENT 'Type de frais (DOCUMENTATION, ISPS, HAYON, ASSURANCE, etc.)',
|
||||
description VARCHAR(200) COMMENT 'Description détaillée du frais',
|
||||
montant DECIMAL(10,2) NOT NULL COMMENT 'Montant du frais',
|
||||
unite_facturation ENUM('LS', 'KG', 'M3', 'PALETTE', 'POURCENTAGE') NOT NULL COMMENT 'Unité de facturation',
|
||||
montant_minimum DECIMAL(10,2) COMMENT 'Montant minimum pour ce frais',
|
||||
obligatoire BOOLEAN DEFAULT FALSE COMMENT 'Frais obligatoire ou optionnel',
|
||||
applicable_marchandise_dangereuse BOOLEAN DEFAULT FALSE COMMENT 'Applicable aux marchandises dangereuses',
|
||||
|
||||
-- Clé étrangère
|
||||
FOREIGN KEY (grille_id) REFERENCES grilles_tarifaires(id) ON DELETE CASCADE,
|
||||
|
||||
-- Index pour les recherches
|
||||
INDEX idx_grille_type (grille_id, type_frais),
|
||||
INDEX idx_obligatoire (obligatoire),
|
||||
INDEX idx_marchandise_dangereuse (applicable_marchandise_dangereuse)
|
||||
);
|
||||
|
||||
-- Table des surcharges pour marchandises dangereuses
|
||||
CREATE TABLE surcharges_dangereuses (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
grille_id BIGINT NOT NULL COMMENT 'Référence vers la grille tarifaire',
|
||||
classe_adr VARCHAR(10) COMMENT 'Classe ADR (1, 2.1, 3, 4.1, etc.)',
|
||||
un_number VARCHAR(10) COMMENT 'Numéro UN pour identification spécifique',
|
||||
surcharge DECIMAL(10,2) NOT NULL COMMENT 'Montant de la surcharge',
|
||||
unite_facturation ENUM('LS', 'KG', 'COLIS') NOT NULL COMMENT 'Unité de facturation',
|
||||
minimum DECIMAL(10,2) COMMENT 'Surcharge minimum',
|
||||
|
||||
-- Clé étrangère
|
||||
FOREIGN KEY (grille_id) REFERENCES grilles_tarifaires(id) ON DELETE CASCADE,
|
||||
|
||||
-- Index pour les recherches
|
||||
INDEX idx_grille_classe (grille_id, classe_adr),
|
||||
INDEX idx_grille_un_number (grille_id, un_number),
|
||||
INDEX idx_classe_adr (classe_adr)
|
||||
);
|
||||
|
||||
-- ================================
|
||||
-- Commentaires sur le modèle de données
|
||||
-- ================================
|
||||
|
||||
-- Les grilles tarifaires permettent de:
|
||||
-- 1. Définir des tarifs spécifiques par route (origine/destination)
|
||||
-- 2. Gérer plusieurs transporteurs avec leurs propres grilles
|
||||
-- 3. Créer 3 types d'offres (RAPIDE, STANDARD, ECONOMIQUE)
|
||||
-- 4. Appliquer des périodes de validité
|
||||
-- 5. Personnaliser par ville ou port spécifique
|
||||
|
||||
-- Les tarifs de fret supportent:
|
||||
-- 1. Barèmes progressifs selon poids ou volume
|
||||
-- 2. Différentes unités de facturation (kg, m³, colis, forfait)
|
||||
-- 3. Minima de facturation
|
||||
-- 4. Poids volumétrique vs poids réel
|
||||
|
||||
-- Les frais additionnels incluent:
|
||||
-- 1. Frais obligatoires (documentation, ISPS, manutention)
|
||||
-- 2. Services optionnels (assurance, hayon, sangles)
|
||||
-- 3. Frais spécifiques aux marchandises dangereuses
|
||||
-- 4. Pourcentages ou montants fixes
|
||||
|
||||
-- Les surcharges dangereuses permettent:
|
||||
-- 1. Tarification spécifique par classe ADR
|
||||
-- 2. Identification par numéro UN
|
||||
-- 3. Surcharges forfaitaires ou au poids/colis
|
||||
@ -0,0 +1,231 @@
|
||||
package com.dh7789dev.xpeditis.repository;
|
||||
|
||||
import com.dh7789dev.xpeditis.entity.GrilleTarifaireEntity;
|
||||
import com.dh7789dev.xpeditis.entity.TarifFretEntity;
|
||||
import com.dh7789dev.xpeditis.entity.FraisAdditionnelsEntity;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
@DataJpaTest
|
||||
@DisplayName("GrilleTarifaireJpaRepository - Tests d'intégration")
|
||||
class GrilleTarifaireJpaRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private TestEntityManager entityManager;
|
||||
|
||||
@Autowired
|
||||
private GrilleTarifaireJpaRepository repository;
|
||||
|
||||
private GrilleTarifaireEntity grilleStandard;
|
||||
private GrilleTarifaireEntity grilleRapide;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Créer et persister des grilles de test
|
||||
grilleStandard = creerGrilleStandard();
|
||||
grilleRapide = creerGrilleRapide();
|
||||
|
||||
entityManager.persistAndFlush(grilleStandard);
|
||||
entityManager.persistAndFlush(grilleRapide);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit trouver les grilles applicables par route et date")
|
||||
void doitTrouverGrillesApplicablesParRouteEtDate() {
|
||||
// When
|
||||
List<GrilleTarifaireEntity> grilles = repository.findGrillesApplicables(
|
||||
GrilleTarifaireEntity.TypeService.EXPORT,
|
||||
"FRA",
|
||||
"CHN",
|
||||
LocalDate.now()
|
||||
);
|
||||
|
||||
// Then
|
||||
assertThat(grilles).hasSize(2);
|
||||
assertThat(grilles)
|
||||
.extracting(GrilleTarifaireEntity::getNomGrille)
|
||||
.containsExactlyInAnyOrder("Test Grille Standard", "Test Grille Rapide");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit filtrer par type de service spécifique")
|
||||
void doitFiltrerParTypeServiceSpecifique() {
|
||||
// When
|
||||
List<GrilleTarifaireEntity> grillesStandard = repository.findByServiceTypeAndRoute(
|
||||
GrilleTarifaireEntity.TypeService.EXPORT,
|
||||
GrilleTarifaireEntity.ServiceType.STANDARD,
|
||||
"FRA",
|
||||
"CHN",
|
||||
LocalDate.now()
|
||||
);
|
||||
|
||||
// Then
|
||||
assertThat(grillesStandard).hasSize(1);
|
||||
assertThat(grillesStandard.get(0).getNomGrille()).isEqualTo("Test Grille Standard");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Ne doit pas trouver de grilles pour une date hors validité")
|
||||
void neDοitPasTrouverGrillesHorsValidite() {
|
||||
// When
|
||||
List<GrilleTarifaireEntity> grilles = repository.findGrillesApplicables(
|
||||
GrilleTarifaireEntity.TypeService.EXPORT,
|
||||
"FRA",
|
||||
"CHN",
|
||||
LocalDate.now().plusYears(2) // Date future hors validité
|
||||
);
|
||||
|
||||
// Then
|
||||
assertThat(grilles).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit trouver les grilles par transporteur et dates")
|
||||
void doitTrouverGrillesParTransporteurEtDates() {
|
||||
// When
|
||||
List<GrilleTarifaireEntity> grilles = repository
|
||||
.findByTransporteurAndValiditeDebutLessThanEqualAndValiditeFinGreaterThanEqual(
|
||||
"LESCHACO",
|
||||
LocalDate.now(),
|
||||
LocalDate.now()
|
||||
);
|
||||
|
||||
// Then
|
||||
assertThat(grilles).hasSize(2);
|
||||
assertThat(grilles)
|
||||
.allMatch(g -> "LESCHACO".equals(g.getTransporteur()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit compter correctement les grilles actives par transporteur")
|
||||
void doitCompterCorrectementGrillesActivesParTransporteur() {
|
||||
// When
|
||||
Long count = repository.countActiveGridsByTransporteur("LESCHACO", LocalDate.now());
|
||||
|
||||
// Then
|
||||
assertThat(count).isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit sauvegarder une grille avec relations en cascade")
|
||||
void doitSauvegarderGrilleAvecRelations() {
|
||||
// Given
|
||||
GrilleTarifaireEntity nouvelleGrille = new GrilleTarifaireEntity();
|
||||
nouvelleGrille.setNomGrille("Nouvelle Grille Test");
|
||||
nouvelleGrille.setTransporteur("MSC");
|
||||
nouvelleGrille.setTypeService(GrilleTarifaireEntity.TypeService.IMPORT);
|
||||
nouvelleGrille.setOriginePays("CHN");
|
||||
nouvelleGrille.setDestinationPays("FRA");
|
||||
nouvelleGrille.setModeTransport(GrilleTarifaireEntity.ModeTransport.MARITIME);
|
||||
nouvelleGrille.setServiceType(GrilleTarifaireEntity.ServiceType.STANDARD);
|
||||
nouvelleGrille.setValiditeDebut(LocalDate.now());
|
||||
nouvelleGrille.setValiditeFin(LocalDate.now().plusMonths(6));
|
||||
|
||||
// Ajouter un tarif de fret
|
||||
TarifFretEntity tarif = new TarifFretEntity();
|
||||
tarif.setGrille(nouvelleGrille);
|
||||
tarif.setPoidsMin(BigDecimal.ZERO);
|
||||
tarif.setPoidsMax(BigDecimal.valueOf(500));
|
||||
tarif.setTauxUnitaire(BigDecimal.valueOf(3.0));
|
||||
tarif.setUniteFacturation(TarifFretEntity.UniteFacturation.KG);
|
||||
|
||||
nouvelleGrille.getTarifsFret().add(tarif);
|
||||
|
||||
// Ajouter des frais additionnels
|
||||
FraisAdditionnelsEntity frais = new FraisAdditionnelsEntity();
|
||||
frais.setGrille(nouvelleGrille);
|
||||
frais.setTypeFrais("DOCUMENTATION");
|
||||
frais.setMontant(BigDecimal.valueOf(40));
|
||||
frais.setUniteFacturation(FraisAdditionnelsEntity.UniteFacturation.LS);
|
||||
frais.setObligatoire(true);
|
||||
|
||||
nouvelleGrille.getFraisAdditionnels().add(frais);
|
||||
|
||||
// When
|
||||
GrilleTarifaireEntity grilleSauvee = repository.saveAndFlush(nouvelleGrille);
|
||||
|
||||
// Then
|
||||
assertThat(grilleSauvee.getId()).isNotNull();
|
||||
assertThat(grilleSauvee.getTarifsFret()).hasSize(1);
|
||||
assertThat(grilleSauvee.getFraisAdditionnels()).hasSize(1);
|
||||
|
||||
// Vérifier que les relations sont bien établies
|
||||
assertThat(grilleSauvee.getTarifsFret().get(0).getGrille().getId())
|
||||
.isEqualTo(grilleSauvee.getId());
|
||||
assertThat(grilleSauvee.getFraisAdditionnels().get(0).getGrille().getId())
|
||||
.isEqualTo(grilleSauvee.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Doit supprimer une grille et ses relations en cascade")
|
||||
void doitSupprimerGrilleEtRelationsEnCascade() {
|
||||
// Given
|
||||
Long grilleId = grilleStandard.getId();
|
||||
|
||||
// Vérifier que la grille existe avant suppression
|
||||
assertThat(repository.existsById(grilleId)).isTrue();
|
||||
|
||||
// When
|
||||
repository.deleteById(grilleId);
|
||||
repository.flush();
|
||||
|
||||
// Then
|
||||
assertThat(repository.existsById(grilleId)).isFalse();
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Méthodes utilitaires
|
||||
// ================================
|
||||
|
||||
private GrilleTarifaireEntity creerGrilleStandard() {
|
||||
GrilleTarifaireEntity grille = new GrilleTarifaireEntity();
|
||||
grille.setNomGrille("Test Grille Standard");
|
||||
grille.setTransporteur("LESCHACO");
|
||||
grille.setTypeService(GrilleTarifaireEntity.TypeService.EXPORT);
|
||||
grille.setOriginePays("FRA");
|
||||
grille.setOrigineVille("Fos Sur Mer");
|
||||
grille.setDestinationPays("CHN");
|
||||
grille.setDestinationVille("Shanghai");
|
||||
grille.setIncoterm("FOB");
|
||||
grille.setModeTransport(GrilleTarifaireEntity.ModeTransport.MARITIME);
|
||||
grille.setServiceType(GrilleTarifaireEntity.ServiceType.STANDARD);
|
||||
grille.setTransitTimeMin(25);
|
||||
grille.setTransitTimeMax(30);
|
||||
grille.setValiditeDebut(LocalDate.now().minusDays(30));
|
||||
grille.setValiditeFin(LocalDate.now().plusDays(90));
|
||||
grille.setDevise("EUR");
|
||||
|
||||
return grille;
|
||||
}
|
||||
|
||||
private GrilleTarifaireEntity creerGrilleRapide() {
|
||||
GrilleTarifaireEntity grille = new GrilleTarifaireEntity();
|
||||
grille.setNomGrille("Test Grille Rapide");
|
||||
grille.setTransporteur("LESCHACO");
|
||||
grille.setTypeService(GrilleTarifaireEntity.TypeService.EXPORT);
|
||||
grille.setOriginePays("FRA");
|
||||
grille.setOrigineVille("Fos Sur Mer");
|
||||
grille.setDestinationPays("CHN");
|
||||
grille.setDestinationVille("Shanghai");
|
||||
grille.setIncoterm("FOB");
|
||||
grille.setModeTransport(GrilleTarifaireEntity.ModeTransport.MARITIME);
|
||||
grille.setServiceType(GrilleTarifaireEntity.ServiceType.RAPIDE);
|
||||
grille.setTransitTimeMin(18);
|
||||
grille.setTransitTimeMax(22);
|
||||
grille.setValiditeDebut(LocalDate.now().minusDays(30));
|
||||
grille.setValiditeFin(LocalDate.now().plusDays(90));
|
||||
grille.setDevise("EUR");
|
||||
|
||||
return grille;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user