diff --git a/Xpeditis_API_Collection.postman_collection.json b/Xpeditis_API_Collection.postman_collection.json index 2233ca1..638b5e8 100644 --- a/Xpeditis_API_Collection.postman_collection.json +++ b/Xpeditis_API_Collection.postman_collection.json @@ -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": [ diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/DevisRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/DevisRestController.java new file mode 100644 index 0000000..1723b06 --- /dev/null +++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/DevisRestController.java @@ -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 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 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()); + } + } +} \ No newline at end of file diff --git a/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/GrilleTarifaireRestController.java b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/GrilleTarifaireRestController.java new file mode 100644 index 0000000..1f5e4ef --- /dev/null +++ b/application/src/main/java/com/dh7789dev/xpeditis/controller/api/v1/GrilleTarifaireRestController.java @@ -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 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 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 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 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 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 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")); + } + } +} \ No newline at end of file diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/XpeditisApplication.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/XpeditisApplication.java index cc5beae..d09ef9b 100755 --- a/bootstrap/src/main/java/com/dh7789dev/xpeditis/XpeditisApplication.java +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/XpeditisApplication.java @@ -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 diff --git a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java index fd8684f..c74f135 100755 --- a/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java +++ b/bootstrap/src/main/java/com/dh7789dev/xpeditis/configuration/SecurityConfiguration.java @@ -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 = { diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/DevisCalculService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/DevisCalculService.java new file mode 100644 index 0000000..119fdd7 --- /dev/null +++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/DevisCalculService.java @@ -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); +} \ No newline at end of file diff --git a/domain/api/src/main/java/com/dh7789dev/xpeditis/GrilleTarifaireService.java b/domain/api/src/main/java/com/dh7789dev/xpeditis/GrilleTarifaireService.java new file mode 100644 index 0000000..c3f00ff --- /dev/null +++ b/domain/api/src/main/java/com/dh7789dev/xpeditis/GrilleTarifaireService.java @@ -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 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 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 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 importerDepuisJson(List 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 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 validerFichier(org.springframework.web.multipart.MultipartFile file) throws java.io.IOException; +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DemandeDevis.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DemandeDevis.java new file mode 100644 index 0000000..9e00cb8 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/DemandeDevis.java @@ -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 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; + } +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/FraisAdditionnels.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/FraisAdditionnels.java new file mode 100644 index 0000000..1d67277 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/FraisAdditionnels.java @@ -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 + } +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/GrilleTarifaire.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/GrilleTarifaire.java new file mode 100644 index 0000000..773ccd0 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/GrilleTarifaire.java @@ -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 tarifsFret; + private List fraisAdditionnels; + private List surchargesDangereuses; + + public enum TypeService { + IMPORT, EXPORT + } + + public enum ModeTransport { + MARITIME, AERIEN, ROUTIER, FERROVIAIRE + } + + public enum ServiceType { + RAPIDE, STANDARD, ECONOMIQUE + } +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/OffreCalculee.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/OffreCalculee.java new file mode 100644 index 0000000..4cd05d6 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/OffreCalculee.java @@ -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 servicesInclus; + private DetailPrix detailPrix; + private LocalDate validite; + private List conditions; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class DetailPrix { + private BigDecimal fretBase; + private Map fraisFixes; + private Map servicesOptionnels; + private BigDecimal surchargeDangereuse; + private BigDecimal coefficientService; + } +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/ReponseDevis.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/ReponseDevis.java new file mode 100644 index 0000000..22f32b0 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/ReponseDevis.java @@ -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 offres; + private Recommandation recommandation; + private List 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; + } +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/SurchargeDangereuse.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/SurchargeDangereuse.java new file mode 100644 index 0000000..853bf68 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/SurchargeDangereuse.java @@ -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 + } +} \ No newline at end of file diff --git a/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/TarifFret.java b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/TarifFret.java new file mode 100644 index 0000000..339b2d6 --- /dev/null +++ b/domain/data/src/main/java/com/dh7789dev/xpeditis/dto/app/TarifFret.java @@ -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 + } +} \ No newline at end of file diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/DevisCalculServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/DevisCalculServiceImpl.java new file mode 100644 index 0000000..00215d6 --- /dev/null +++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/DevisCalculServiceImpl.java @@ -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 grillesApplicables = grilleTarifaireService.trouverGrillesApplicables(demandeDevis); + + if (grillesApplicables.isEmpty()) { + throw new IllegalStateException("Aucune grille tarifaire applicable pour cette demande"); + } + + // Générer les 3 offres + List 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 genererTroisOffres( + DemandeDevis demande, + List grilles, + ReponseDevis.ColisageResume colisage) { + + List 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 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 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 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 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 calculerFraisFixes(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) { + Map 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 calculerServicesOptionnels(GrilleTarifaire grille, DemandeDevis demande, ReponseDevis.ColisageResume colisage) { + Map 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 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 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 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 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"); + } + } + } +} \ No newline at end of file diff --git a/domain/service/src/main/java/com/dh7789dev/xpeditis/GrilleTarifaireServiceImpl.java b/domain/service/src/main/java/com/dh7789dev/xpeditis/GrilleTarifaireServiceImpl.java new file mode 100644 index 0000000..19de87e --- /dev/null +++ b/domain/service/src/main/java/com/dh7789dev/xpeditis/GrilleTarifaireServiceImpl.java @@ -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 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 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 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 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 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 importerDepuisJson(List 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 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 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 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 validerFichier(MultipartFile file) throws IOException { + log.info("Validation fichier - Nom: {}, Taille: {} bytes", file.getOriginalFilename(), file.getSize()); + + Map resultat = new HashMap<>(); + List erreurs = new ArrayList<>(); + List 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 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; + } +} \ No newline at end of file diff --git a/domain/service/src/test/java/com/dh7789dev/xpeditis/DevisCalculServiceImplTest.java b/domain/service/src/test/java/com/dh7789dev/xpeditis/DevisCalculServiceImplTest.java new file mode 100644 index 0000000..2d95dcd --- /dev/null +++ b/domain/service/src/test/java/com/dh7789dev/xpeditis/DevisCalculServiceImplTest.java @@ -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 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; + } +} \ No newline at end of file diff --git a/domain/spi/src/main/java/com/dh7789dev/xpeditis/GrilleTarifaireRepository.java b/domain/spi/src/main/java/com/dh7789dev/xpeditis/GrilleTarifaireRepository.java new file mode 100644 index 0000000..ff1732e --- /dev/null +++ b/domain/spi/src/main/java/com/dh7789dev/xpeditis/GrilleTarifaireRepository.java @@ -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 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 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 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 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 saveAll(List grilles); + + /** + * Trouve toutes les grilles tarifaires + * + * @return la liste de toutes les grilles + */ + List findAll(); +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/GrilleTarifaireDao.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/GrilleTarifaireDao.java new file mode 100644 index 0000000..0fca0e9 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/dao/GrilleTarifaireDao.java @@ -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 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 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 findByServiceTypeAndRoute( + String typeService, + String serviceType, + String originePays, + String destinationPays, + LocalDate dateValidite) { + + GrilleTarifaireEntity.TypeService typeServiceEnum = parseTypeService(typeService); + GrilleTarifaireEntity.ServiceType serviceTypeEnum = parseServiceType(serviceType); + + List 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 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 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 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 saveAll(List grilles) { + log.info("Sauvegarde de {} grilles tarifaires", grilles.size()); + + List entities = grilles.stream() + .map(mapper::dtoToEntity) + .collect(Collectors.toList()); + + List savedEntities = jpaRepository.saveAll(entities); + + return savedEntities.stream() + .map(mapper::entityToDto) + .collect(Collectors.toList()); + } + + @Override + public List findAll() { + log.debug("Recherche de toutes les grilles tarifaires"); + + List 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); + } + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/FraisAdditionnelsEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/FraisAdditionnelsEntity.java new file mode 100644 index 0000000..d50cd8c --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/FraisAdditionnelsEntity.java @@ -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 + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/GrilleTarifaireEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/GrilleTarifaireEntity.java new file mode 100644 index 0000000..2278608 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/GrilleTarifaireEntity.java @@ -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 tarifsFret = new ArrayList<>(); + + @OneToMany(mappedBy = "grille", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List fraisAdditionnels = new ArrayList<>(); + + @OneToMany(mappedBy = "grille", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List surchargesDangereuses = new ArrayList<>(); + + public enum TypeService { + IMPORT, EXPORT + } + + public enum ModeTransport { + MARITIME, AERIEN, ROUTIER, FERROVIAIRE + } + + public enum ServiceType { + RAPIDE, STANDARD, ECONOMIQUE + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/SurchargeDangereuse.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/SurchargeDangereuse.java new file mode 100644 index 0000000..a755338 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/SurchargeDangereuse.java @@ -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 + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/TarifFretEntity.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/TarifFretEntity.java new file mode 100644 index 0000000..8e2ec4a --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/entity/TarifFretEntity.java @@ -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 + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/GrilleTarifaireMapper.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/GrilleTarifaireMapper.java new file mode 100644 index 0000000..5e29659 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/mapper/GrilleTarifaireMapper.java @@ -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 tarifsFret = dto.getTarifsFret().stream() + .map(this::tarifFretDtoToEntity) + .collect(Collectors.toList()); + tarifsFret.forEach(tarif -> tarif.setGrille(entity)); + entity.setTarifsFret(tarifsFret); + } + + if (dto.getFraisAdditionnels() != null) { + List fraisAdditionnels = dto.getFraisAdditionnels().stream() + .map(this::fraisAdditionnelsDtoToEntity) + .collect(Collectors.toList()); + fraisAdditionnels.forEach(frais -> frais.setGrille(entity)); + entity.setFraisAdditionnels(fraisAdditionnels); + } + + if (dto.getSurchargesDangereuses() != null) { + List 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()); + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/FraisAdditionnelsJpaRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/FraisAdditionnelsJpaRepository.java new file mode 100644 index 0000000..a818788 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/FraisAdditionnelsJpaRepository.java @@ -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 { + + /** + * Trouve tous les frais additionnels pour une grille donnée + */ + List findByGrilleIdOrderByTypeFrais(Long grilleId); + + /** + * Trouve les frais obligatoires pour une grille + */ + List findByGrilleIdAndObligatoireTrue(Long grilleId); + + /** + * Trouve les frais par type pour une grille + */ + List 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 findFraisApplicablesMarchandiseDangereuse(@Param("grilleId") Long grilleId); + + /** + * Supprime tous les frais additionnels d'une grille + */ + void deleteByGrilleId(Long grilleId); +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/GrilleTarifaireJpaRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/GrilleTarifaireJpaRepository.java new file mode 100644 index 0000000..76899d4 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/GrilleTarifaireJpaRepository.java @@ -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 { + + /** + * 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 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 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 findByTransporteurAndValiditeDebutLessThanEqualAndValiditeFinGreaterThanEqual( + String transporteur, + LocalDate dateDebut, + LocalDate dateFin + ); + + /** + * Trouve les grilles par route + */ + List 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); +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/SurchargeDangereuseJpaRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/SurchargeDangereuseJpaRepository.java new file mode 100644 index 0000000..a05abf5 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/SurchargeDangereuseJpaRepository.java @@ -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 { + + /** + * Trouve toutes les surcharges pour une grille donnée + */ + List findByGrilleIdOrderByClasseAdr(Long grilleId); + + /** + * Trouve une surcharge par classe ADR pour une grille + */ + Optional findByGrilleIdAndClasseAdr(Long grilleId, String classeAdr); + + /** + * Trouve une surcharge par numéro UN pour une grille + */ + Optional findByGrilleIdAndUnNumber(Long grilleId, String unNumber); + + /** + * Trouve les surcharges par classe ADR + */ + List findByClasseAdr(String classeAdr); + + /** + * Supprime toutes les surcharges d'une grille + */ + void deleteByGrilleId(Long grilleId); +} \ No newline at end of file diff --git a/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/TarifFretJpaRepository.java b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/TarifFretJpaRepository.java new file mode 100644 index 0000000..4071d77 --- /dev/null +++ b/infrastructure/src/main/java/com/dh7789dev/xpeditis/repository/TarifFretJpaRepository.java @@ -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 { + + /** + * Trouve tous les tarifs de fret pour une grille donnée + */ + List 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 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 findTarifApplicableByVolume( + @Param("grilleId") Long grilleId, + @Param("volume") BigDecimal volume + ); + + /** + * Supprime tous les tarifs d'une grille + */ + void deleteByGrilleId(Long grilleId); +} \ No newline at end of file diff --git a/infrastructure/src/main/resources/db/migration/data/V3.1__GRILLES_TARIFAIRES_SAMPLE_DATA.sql b/infrastructure/src/main/resources/db/migration/data/V3.1__GRILLES_TARIFAIRES_SAMPLE_DATA.sql new file mode 100644 index 0000000..258d33e --- /dev/null +++ b/infrastructure/src/main/resources/db/migration/data/V3.1__GRILLES_TARIFAIRES_SAMPLE_DATA.sql @@ -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. \ No newline at end of file diff --git a/infrastructure/src/main/resources/db/migration/structure/V3__GRILLES_TARIFAIRES_SYSTEM.sql b/infrastructure/src/main/resources/db/migration/structure/V3__GRILLES_TARIFAIRES_SYSTEM.sql new file mode 100644 index 0000000..ed8cf76 --- /dev/null +++ b/infrastructure/src/main/resources/db/migration/structure/V3__GRILLES_TARIFAIRES_SYSTEM.sql @@ -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 \ No newline at end of file diff --git a/infrastructure/src/test/java/com/dh7789dev/xpeditis/repository/GrilleTarifaireJpaRepositoryTest.java b/infrastructure/src/test/java/com/dh7789dev/xpeditis/repository/GrilleTarifaireJpaRepositoryTest.java new file mode 100644 index 0000000..7e6e01b --- /dev/null +++ b/infrastructure/src/test/java/com/dh7789dev/xpeditis/repository/GrilleTarifaireJpaRepositoryTest.java @@ -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 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 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 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 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; + } +} \ No newline at end of file