From a9275a012a69a41223a454ba00ffd398cc33fe0d Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 19 Jul 2025 22:10:24 +0200 Subject: [PATCH] Added Premise Controller Integration Tests --- .../controller/GlobalExceptionHandler.java | 55 +- .../calculation/PremiseController.java | 104 +- .../configuration/NodeController.java | 19 +- .../configuration/RateController.java | 11 +- .../lcc/controller/users/GroupController.java | 5 +- .../lcc/controller/users/UserController.java | 7 +- .../calculation/edit/PremiseDetailDTO.java | 3 + .../nodes/update/NodeUpdateDTO.java | 70 +- .../nodes/view/NodeDetailDTO.java | 12 + .../configuration/rates/ContainerRateDTO.java | 2 + .../avatic/lcc/dto/generic/LocationDTO.java | 2 + .../de/avatic/lcc/dto/generic/NodeDTO.java | 12 + .../java/de/avatic/lcc/model/nodes/Node.java | 2 + .../lcc/model/premises/PremiseListEntry.java | 9 + .../lcc/repositories/MaterialRepository.java | 28 + .../lcc/repositories/NodeRepository.java | 84 +- .../premise/DestinationRepository.java | 15 +- .../premise/PremiseRepository.java | 277 ++- .../premise/RouteNodeRepository.java | 13 +- .../rates/ContainerRateRepository.java | 10 +- .../rates/MatrixRateRepository.java | 6 +- .../rates/ValidityPeriodRepository.java | 20 +- .../users/UserNodeRepository.java | 53 +- .../avatic/lcc/service/CustomApiService.java | 6 +- .../service/access/ContainerRateService.java | 8 +- .../lcc/service/access/MatrixRateService.java | 7 +- .../lcc/service/access/NodeService.java | 11 +- .../lcc/service/access/PremisesService.java | 7 +- .../lcc/service/access/UserNodeService.java | 18 +- .../service/access/ValidityPeriodService.java | 5 +- .../lcc/service/bulk/BulkExportService.java | 3 +- .../calculation/ChangeSupplierService.java | 1 - .../calculation/PremiseCreationService.java | 254 ++- .../PremiseSearchStringAnalyzerService.java | 12 +- .../steps/CustomCostCalculationService.java | 8 +- .../generic/DimensionTransformer.java | 19 +- .../transformer/generic/NodeTransformer.java | 1 + .../nodes/NodeDetailTransformer.java | 5 +- .../nodes/NodeUpdateDTOTransformer.java | 15 +- .../premise/PremiseTransformer.java | 10 +- .../badrequest/InvalidArgumentException.java | 15 + .../badrequest/NotFoundException.java | 38 +- .../exception/base/ForbiddenException.java | 42 + src/main/resources/master_data/03-nodes.sql | 426 ++-- .../master_data/04-predecessor-nodes.sql | 7 +- .../master_data/08-data-containerrate.sql | 1332 +++++++++++++ ...py => excel_to_sql_converter_materials.py} | 0 src/main/resources/schema.sql | 115 +- .../PremiseControllerIntegrationTest.java | 1180 +++++++++++ .../PremiseControllerTestData.java | 318 +++ .../NodeControllerIntegrationTest.java | 450 +++++ ...RateControllerAdvancedIntegrationTest.java | 262 +++ .../RateControllerIntegrationTest.java | 370 ++++ .../resources/master_data/alldata-cleanup.sql | 76 + src/test/resources/master_data/alldata.sql | 1765 ++++++++++++++--- .../countries_properties-cleanup.sql | 8 + .../material_packaging-cleanup.sql | 10 +- .../master_data/material_packaging.sql | 2 +- .../resources/master_data/nodes-cleanup.sql | 5 + src/test/resources/master_data/nodes.sql | 1603 ++------------- .../master_data/premises-cleanup.sql | 12 + src/test/resources/master_data/premises.sql | 458 +++++ .../reduced_rate_setup-cleanup.sql | 38 + .../master_data/reduced_rate_setup.sql | 95 + .../resources/master_data/users-cleanup.sql | 23 + src/test/resources/master_data/users.sql | 54 + src/test/resources/schema.sql | 561 ------ 67 files changed, 7747 insertions(+), 2727 deletions(-) create mode 100644 src/main/java/de/avatic/lcc/util/exception/badrequest/InvalidArgumentException.java create mode 100644 src/main/java/de/avatic/lcc/util/exception/base/ForbiddenException.java create mode 100644 src/main/resources/master_data/08-data-containerrate.sql rename src/main/resources/master_data/{excel_to_sql_converter.py => excel_to_sql_converter_materials.py} (100%) create mode 100644 src/test/java/de/avatic/lcc/controller/calculation/PremiseControllerIntegrationTest.java create mode 100644 src/test/java/de/avatic/lcc/controller/calculation/PremiseControllerTestData.java create mode 100644 src/test/java/de/avatic/lcc/controller/configuration/NodeControllerIntegrationTest.java create mode 100644 src/test/java/de/avatic/lcc/controller/configuration/RateControllerAdvancedIntegrationTest.java create mode 100644 src/test/java/de/avatic/lcc/controller/configuration/RateControllerIntegrationTest.java create mode 100644 src/test/resources/master_data/alldata-cleanup.sql create mode 100644 src/test/resources/master_data/premises-cleanup.sql create mode 100644 src/test/resources/master_data/premises.sql create mode 100644 src/test/resources/master_data/reduced_rate_setup-cleanup.sql create mode 100644 src/test/resources/master_data/reduced_rate_setup.sql create mode 100644 src/test/resources/master_data/users-cleanup.sql create mode 100644 src/test/resources/master_data/users.sql delete mode 100644 src/test/resources/schema.sql diff --git a/src/main/java/de/avatic/lcc/controller/GlobalExceptionHandler.java b/src/main/java/de/avatic/lcc/controller/GlobalExceptionHandler.java index ddf6cc3..c928beb 100644 --- a/src/main/java/de/avatic/lcc/controller/GlobalExceptionHandler.java +++ b/src/main/java/de/avatic/lcc/controller/GlobalExceptionHandler.java @@ -3,13 +3,16 @@ package de.avatic.lcc.controller; import de.avatic.lcc.dto.error.ErrorDTO; import de.avatic.lcc.dto.error.ErrorResponseDTO; import de.avatic.lcc.util.exception.base.BadRequestException; +import de.avatic.lcc.util.exception.base.ForbiddenException; import jakarta.validation.ConstraintViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import java.util.HashMap; @@ -18,15 +21,40 @@ import java.util.Map; @ControllerAdvice public class GlobalExceptionHandler { + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleValidationExceptions( + HttpMessageNotReadableException exception) { + ErrorDTO error = new ErrorDTO( + exception.getClass().getSimpleName(), + "Malformed Request", + exception.getMessage(), + new HashMap<>() {{ + put("errorMessage", exception.getMessage()); + put("stackTrace", exception.getStackTrace()); + }} + ); + return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidationExceptions( - MethodArgumentNotValidException ex) { - Map errors = new HashMap<>(); - ex.getBindingResult().getFieldErrors().forEach(error -> - errors.put(error.getField(), error.getDefaultMessage())); - return ResponseEntity.badRequest().body(errors); + public ResponseEntity handleValidationExceptions( + MethodArgumentNotValidException exception) { + + ErrorDTO error = new ErrorDTO( + exception.getClass().getSimpleName(), + "Constraint Violation", + exception.getMessage(), + new HashMap<>() {{ + put("errorMessage", exception.getMessage()); + put("stackTrace", exception.getStackTrace()); + }} + ); + + return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST); } // @ResponseStatus(HttpStatus.BAD_REQUEST) @@ -82,7 +110,22 @@ public class GlobalExceptionHandler { return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.BAD_REQUEST); } + @ResponseStatus(HttpStatus.FORBIDDEN) + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbiddenException(ForbiddenException exception) { // + ErrorDTO error = new ErrorDTO( + exception.getClass().getSimpleName(), + "Forbidden Error", + exception.getMessage(), + new HashMap<>() {{ + put("errorMessage", exception.getMessage()); + put("stackTrace", exception.getStackTrace()); + }} + ); + + return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.FORBIDDEN); + } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(ConstraintViolationException.class) diff --git a/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java b/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java index cdb57c6..54cca2f 100644 --- a/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java +++ b/src/main/java/de/avatic/lcc/controller/calculation/PremiseController.java @@ -1,31 +1,38 @@ package de.avatic.lcc.controller.calculation; -import de.avatic.lcc.dto.bulk.BulkStatus; import de.avatic.lcc.dto.calculation.CalculationStatus; -import de.avatic.lcc.dto.calculation.edit.SetDataDTO; -import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO; -import de.avatic.lcc.dto.calculation.edit.*; -import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO; import de.avatic.lcc.dto.calculation.DestinationDTO; -import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO; -import de.avatic.lcc.dto.calculation.edit.masterData.*; import de.avatic.lcc.dto.calculation.PremiseDTO; +import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO; +import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; +import de.avatic.lcc.dto.calculation.edit.SetDataDTO; +import de.avatic.lcc.dto.calculation.edit.destination.DestinationCreateDTO; +import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO; +import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO; +import de.avatic.lcc.dto.calculation.edit.masterData.PackagingUpdateDTO; +import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO; +import de.avatic.lcc.dto.generic.LocationDTO; import de.avatic.lcc.service.access.DestinationService; +import de.avatic.lcc.service.access.PremisesService; import de.avatic.lcc.service.calculation.ChangeMaterialService; import de.avatic.lcc.service.calculation.ChangeSupplierService; -import de.avatic.lcc.service.calculation.PremiseSearchStringAnalyzerService; import de.avatic.lcc.service.calculation.PremiseCreationService; -import de.avatic.lcc.service.access.PremisesService; +import de.avatic.lcc.service.calculation.PremiseSearchStringAnalyzerService; +import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException; +import jakarta.validation.constraints.Min; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/calculation") +@Validated public class PremiseController { private final PremiseSearchStringAnalyzerService premiseSearchStringAnalyzerService; @@ -44,8 +51,15 @@ public class PremiseController { this.changeMaterialService = changeMaterialService; } - @GetMapping("/view") - public ResponseEntity> listPremises(@RequestParam String filter, @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer limit, @RequestParam(name = "user", required = false) Integer userId, @RequestParam(required = false) Boolean deleted, @RequestParam(required = false) Boolean archived, @RequestParam(required = false) Boolean done) { + @GetMapping({"/view", "/view/"}) + public ResponseEntity> listPremises(@RequestParam(required = false) String filter, + @RequestParam(defaultValue = "20") @Min(1) int limit, + @RequestParam(defaultValue = "1") @Min(1) int page, + @RequestParam(name = "user", required = false) Integer userId, + @RequestParam(required = false) Boolean deleted, + @RequestParam(required = false) Boolean archived, + @RequestParam(required = false) Boolean done) { + var premises = premisesServices.listPremises(filter, page, limit, userId, deleted, archived, done); return ResponseEntity.ok() @@ -55,22 +69,56 @@ public class PremiseController { .body(premises.toList()); } - @GetMapping("/search") + @GetMapping({"/search", "/search/"}) public ResponseEntity findMaterialsAndSuppliers(@RequestParam String search) { return ResponseEntity.ok(premiseSearchStringAnalyzerService.findMaterialAndSuppliers(search)); } - @PostMapping("/create") - public ResponseEntity> createPremises(@RequestParam("material") List materialIds, @RequestParam("supplier") List supplierIds, @RequestParam("user_supplier") List userSupplierIds, @RequestParam("from_scratch") boolean createEmpty) { - return ResponseEntity.ok(premiseCreationService.createPremises(materialIds, supplierIds, userSupplierIds, createEmpty)); + @PostMapping({"/create", "/create/"}) + public ResponseEntity> createPremises(@RequestParam("material") List materialIds, + @RequestParam(value = "supplier", required = false) List supplierIds, + @RequestParam(name = "user_supplier", required = false) List userSupplierIds, + @RequestParam(name = "from_scratch", defaultValue = "true") boolean createEmpty) { + + List suppliers; + List userSuppliers; + List materials; + + try { + suppliers = supplierIds == null ? Collections.emptyList() : supplierIds.stream().map(Integer::parseInt).toList(); + userSuppliers = userSupplierIds == null ? Collections.emptyList() : userSupplierIds.stream().map(Integer::parseInt).toList(); + materials = materialIds.stream().map(Integer::parseInt).toList(); + + if(suppliers.stream().anyMatch(s -> s < 1)) + throw new InvalidArgumentException("Supplier ID must be greater than or equal to 1"); + + if(userSuppliers.stream().anyMatch(s -> s < 1)) + throw new InvalidArgumentException("User supplier ID must be greater than or equal to 1"); + + if(materials.stream().anyMatch(s -> s < 1)) + throw new InvalidArgumentException("Material ID must be greater than or equal to 1"); + + } catch (NumberFormatException e) { + throw new InvalidArgumentException("Invalid material or supplier ID provided. Suppliers: " + + supplierIds + ", userSuppliers: " + userSupplierIds + ", materials: " + materialIds + "."); + } + + if (suppliers.isEmpty() && userSuppliers.isEmpty()) { + throw new InvalidArgumentException("Either suppliers or userSuppliers must be provided. Suppliers: " + + supplierIds + ", userSuppliers: " + userSupplierIds + "."); + } + + return ResponseEntity.ok(premiseCreationService.createPremises(materials, suppliers, userSuppliers, createEmpty)); } - @GetMapping("/edit") + + + @GetMapping({"/edit", "/edit/"}) public ResponseEntity> getPremises(@RequestParam("premiss_ids") List premissIds) { return ResponseEntity.ok(premisesServices.getPremises(premissIds)); } - @PutMapping("/start") + @PutMapping({"/start", "/start/"}) public ResponseEntity startCalculation(@RequestBody List premiseIds) { return ResponseEntity.ok(premisesServices.startCalculation(premiseIds)); } @@ -81,51 +129,51 @@ public class PremiseController { * @param id The unique identifier of the operation (processing_id) to check its status. * @return A ResponseEntity with the bulk processing status payload. */ - @GetMapping("/status/{processing_id}") - public ResponseEntity getUploadStatus(@PathVariable("processing_id") Integer id) { + @GetMapping({"/status/{processing_id}", "/status/{processing_id}/"}) + public ResponseEntity getCalculationStatus(@PathVariable("processing_id") Integer id) { return ResponseEntity.ok(premisesServices.getCalculationStatus(id)); } - @PutMapping("/packaging") + @PutMapping({"/status/{processing_id}", "/status/{processing_id}/"}) public ResponseEntity> updatePackaging(@RequestBody PackagingUpdateDTO packagingDTO) { return ResponseEntity.ok(premisesServices.updatePackaging(packagingDTO)); } - @PostMapping("/material") + @PostMapping({"/material", "/material/"}) public ResponseEntity> updateMaterial(@RequestBody MaterialUpdateDTO materialUpdateDTO) { return ResponseEntity.ok(premisesServices.updateMaterial(materialUpdateDTO)); } - @PutMapping("/price") + @PutMapping({"/price", "/price/"}) public ResponseEntity> updatePrice(@RequestBody PriceUpdateDTO priceUpdateDTO) { return ResponseEntity.ok(premisesServices.updatePrice(priceUpdateDTO)); } - @PostMapping("/destination") + @PostMapping({"/destination", "/destination/"}) public ResponseEntity> createDestination(@RequestBody DestinationCreateDTO destinationCreateDTO) { return ResponseEntity.ok(destinationService.createDestination(destinationCreateDTO)); } - @GetMapping("/destination({id}") + @GetMapping({"/destination({id}", "/destination({id}/"}) public ResponseEntity getDestination(@PathVariable Integer id) { return ResponseEntity.ok(destinationService.getDestination(id)); } - @PutMapping("/destination({id}") + @PutMapping({"/destination({id}", "/destination({id}/"}) public ResponseEntity updateDestination(@PathVariable Integer id, @RequestParam DestinationUpdateDTO destinationUpdateDTO) { destinationService.updateDestination(id, destinationUpdateDTO); return ResponseEntity.ok().build(); } - @DeleteMapping("/destination({id}") + @DeleteMapping({"/destination({id}", "/destination({id}/"}) public ResponseEntity deleteDestination(@PathVariable Integer id) { destinationService.deleteDestinationById(id, false); return ResponseEntity.ok().build(); } - @PutMapping("/supplier") - public ResponseEntity> setSupplier(@RequestBody SetDataDTO setSupplierDTO) { + @PutMapping({"/supplier", "/supplier/"}) + public ResponseEntity> setSupplier(@RequestBody SetDataDTO setSupplierDTO) { return ResponseEntity.ok(changeSupplierService.setSupplier(setSupplierDTO)); } diff --git a/src/main/java/de/avatic/lcc/controller/configuration/NodeController.java b/src/main/java/de/avatic/lcc/controller/configuration/NodeController.java index ca2ea7b..e35fcc7 100644 --- a/src/main/java/de/avatic/lcc/controller/configuration/NodeController.java +++ b/src/main/java/de/avatic/lcc/controller/configuration/NodeController.java @@ -11,13 +11,16 @@ import de.avatic.lcc.service.GeoApiService; import de.avatic.lcc.service.access.NodeService; import de.avatic.lcc.service.access.UserNodeService; import de.avatic.lcc.util.Check; +import jakarta.validation.constraints.Min; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/nodes") +@Validated public class NodeController { private final NodeService nodeService; @@ -31,7 +34,7 @@ public class NodeController { } @GetMapping("/") - public ResponseEntity> listNodes(@RequestParam(required = false) String filter, @RequestParam(required = false) int page, @RequestParam(required = false) int limit) { + public ResponseEntity> listNodes(@RequestParam(required = false) String filter, @RequestParam(defaultValue = "1") @Min(1) Integer page, @RequestParam(defaultValue = "20") @Min(1) Integer limit) { nodeService.listNodes(filter, page, limit); SearchQueryResult nodes = nodeService.listNodes(filter, page, limit); @@ -44,7 +47,7 @@ public class NodeController { } @GetMapping("/search") - public ResponseEntity> searchNodes(@RequestParam(required = false) String filter, @RequestParam(required = false) int limit, @RequestParam(name = "node_type", required = false) NodeType nodeType, @RequestParam(name = "include_user_node", defaultValue = "false", required = false) boolean includeUserNode) { + public ResponseEntity> searchNodes(@RequestParam(required = false) String filter, @RequestParam(defaultValue = "1") @Min(1) int limit, @RequestParam(name = "node_type", required = false) NodeType nodeType, @RequestParam(name = "include_user_node", defaultValue = "false") boolean includeUserNode) { return ResponseEntity.ok(nodeService.searchNode(filter, limit, nodeType, includeUserNode)); } @@ -58,11 +61,11 @@ public class NodeController { return ResponseEntity.ok(nodeService.deleteNode(id)); } - @PutMapping("/{id}") - public ResponseEntity updateNode(@PathVariable Integer id, @RequestBody NodeUpdateDTO node) { - Check.equals(id, node.getId()); - return ResponseEntity.ok(nodeService.updateNode(node)); - } +// @PutMapping("/{id}") +// public ResponseEntity updateNode(@PathVariable Integer id, @RequestBody NodeUpdateDTO node) { +// Check.equals(id, node.getId()); +// return ResponseEntity.ok(nodeService.updateNode(node)); +// } @GetMapping("/locate") public ResponseEntity locateNode(@RequestParam String address) { @@ -70,7 +73,7 @@ public class NodeController { } @PutMapping("/") - public ResponseEntity addUserNode(@RequestParam AddUserNodeDTO node) { + public ResponseEntity addUserNode(@RequestBody AddUserNodeDTO node) { userNodeService.addUserNode(node); return ResponseEntity.ok().build(); } diff --git a/src/main/java/de/avatic/lcc/controller/configuration/RateController.java b/src/main/java/de/avatic/lcc/controller/configuration/RateController.java index 2397e86..a6100f0 100644 --- a/src/main/java/de/avatic/lcc/controller/configuration/RateController.java +++ b/src/main/java/de/avatic/lcc/controller/configuration/RateController.java @@ -8,8 +8,10 @@ import de.avatic.lcc.service.access.ContainerRateService; import de.avatic.lcc.service.access.MatrixRateService; import de.avatic.lcc.service.configuration.RateApprovalService; import de.avatic.lcc.service.access.ValidityPeriodService; +import jakarta.validation.constraints.Min; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; @@ -17,6 +19,7 @@ import java.util.List; @RestController @RequestMapping("/api/rates") +@Validated public class RateController { private final MatrixRateService matrixRateService; @@ -50,8 +53,8 @@ public class RateController { */ @GetMapping("/container") public ResponseEntity> listContainerRates( - @RequestParam(defaultValue = "20") int limit, - @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") @Min(1) int limit, + @RequestParam(defaultValue = "1") @Min(1) int page, @RequestParam(name= "valid", required = false) Integer validityPeriodId, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime validAt) { @@ -97,8 +100,8 @@ public class RateController { */ @GetMapping("/matrix") public ResponseEntity> listMatrixRates( - @RequestParam(defaultValue = "20") int limit, - @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") @Min(1) int limit, + @RequestParam(defaultValue = "1") @Min(1) int page, @RequestParam(required = false) Integer valid, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime validAt) { diff --git a/src/main/java/de/avatic/lcc/controller/users/GroupController.java b/src/main/java/de/avatic/lcc/controller/users/GroupController.java index 333ad67..21c5b3f 100644 --- a/src/main/java/de/avatic/lcc/controller/users/GroupController.java +++ b/src/main/java/de/avatic/lcc/controller/users/GroupController.java @@ -3,6 +3,7 @@ package de.avatic.lcc.controller.users; import de.avatic.lcc.dto.users.GroupDTO; import de.avatic.lcc.repositories.pagination.SearchQueryResult; import de.avatic.lcc.service.users.GroupService; +import jakarta.validation.constraints.Min; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -31,8 +32,8 @@ public class GroupController { * @return A ResponseEntity containing the list of groups and pagination headers. */ @GetMapping("/") - public ResponseEntity> listGroups(@RequestParam(defaultValue = "20") int limit, - @RequestParam(defaultValue = "1") int page) { + public ResponseEntity> listGroups(@RequestParam(defaultValue = "20") @Min(1) int limit, + @RequestParam(defaultValue = "1") @Min(1) int page) { SearchQueryResult groups = groupService.listGroups(page, limit); diff --git a/src/main/java/de/avatic/lcc/controller/users/UserController.java b/src/main/java/de/avatic/lcc/controller/users/UserController.java index b6367b2..8db3b89 100644 --- a/src/main/java/de/avatic/lcc/controller/users/UserController.java +++ b/src/main/java/de/avatic/lcc/controller/users/UserController.java @@ -4,7 +4,9 @@ package de.avatic.lcc.controller.users; import de.avatic.lcc.dto.users.UserDTO; import de.avatic.lcc.repositories.pagination.SearchQueryResult; import de.avatic.lcc.service.users.UserService; +import jakarta.validation.constraints.Min; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -15,6 +17,7 @@ import java.util.List; */ @RestController @RequestMapping("/api/users") +@Validated public class UserController { @@ -33,8 +36,8 @@ public class UserController { */ @GetMapping("/") public ResponseEntity> listUsers( - @RequestParam(defaultValue = "20") int limit, - @RequestParam(defaultValue = "0") int page) { + @RequestParam(defaultValue = "20") @Min(1) int limit, + @RequestParam(defaultValue = "0") @Min(1) int page) { SearchQueryResult users = userService.listUsers(page, limit); diff --git a/src/main/java/de/avatic/lcc/dto/calculation/edit/PremiseDetailDTO.java b/src/main/java/de/avatic/lcc/dto/calculation/edit/PremiseDetailDTO.java index f5d9fa5..390c203 100644 --- a/src/main/java/de/avatic/lcc/dto/calculation/edit/PremiseDetailDTO.java +++ b/src/main/java/de/avatic/lcc/dto/calculation/edit/PremiseDetailDTO.java @@ -1,5 +1,6 @@ package de.avatic.lcc.dto.calculation.edit; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import de.avatic.lcc.dto.calculation.DestinationDTO; import de.avatic.lcc.dto.generic.DimensionDTO; @@ -59,6 +60,7 @@ public class PremiseDetailDTO { this.dimension = dimension; } + @JsonIgnore public Boolean getMixable() { return isMixable; } @@ -67,6 +69,7 @@ public class PremiseDetailDTO { isMixable = mixable; } + @JsonIgnore public Boolean getStackable() { return isStackable; } diff --git a/src/main/java/de/avatic/lcc/dto/configuration/nodes/update/NodeUpdateDTO.java b/src/main/java/de/avatic/lcc/dto/configuration/nodes/update/NodeUpdateDTO.java index 52c3ecd..5bfaa87 100644 --- a/src/main/java/de/avatic/lcc/dto/configuration/nodes/update/NodeUpdateDTO.java +++ b/src/main/java/de/avatic/lcc/dto/configuration/nodes/update/NodeUpdateDTO.java @@ -24,6 +24,74 @@ public class NodeUpdateDTO { private List outboundCountries; public Integer getId() { - return null; + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public CountryDTO getCountry() { + return country; + } + + public void setCountry(CountryDTO country) { + this.country = country; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public List getTypes() { + return types; + } + + public void setTypes(List types) { + this.types = types; + } + + public LocationDTO getLocation() { + return location; + } + + public void setLocation(LocationDTO location) { + this.location = location; + } + + public Boolean getDeprecated() { + return isDeprecated; + } + + public void setDeprecated(Boolean deprecated) { + isDeprecated = deprecated; + } + + public Map getPredecessors() { + return predecessors; + } + + public void setPredecessors(Map predecessors) { + this.predecessors = predecessors; + } + + public List getOutboundCountries() { + return outboundCountries; + } + + public void setOutboundCountries(List outboundCountries) { + this.outboundCountries = outboundCountries; } } diff --git a/src/main/java/de/avatic/lcc/dto/configuration/nodes/view/NodeDetailDTO.java b/src/main/java/de/avatic/lcc/dto/configuration/nodes/view/NodeDetailDTO.java index 3e80159..43c60f7 100644 --- a/src/main/java/de/avatic/lcc/dto/configuration/nodes/view/NodeDetailDTO.java +++ b/src/main/java/de/avatic/lcc/dto/configuration/nodes/view/NodeDetailDTO.java @@ -23,6 +23,9 @@ public class NodeDetailDTO { @JsonProperty("outbound_countries") private List outboundCountries; + @JsonProperty("external_mapping_id") + private String externalMappingId; + public Integer getId() { return id; @@ -72,6 +75,7 @@ public class NodeDetailDTO { this.location = location; } + @JsonProperty("is_deprecated") public Boolean getDeprecated() { return isDeprecated; } @@ -95,4 +99,12 @@ public class NodeDetailDTO { public void setOutboundCountries(List outboundCountries) { this.outboundCountries = outboundCountries; } + + public void setExternalMappingId(String externalMappingId) { + this.externalMappingId = externalMappingId; + } + + public String getExternalMappingId() { + return externalMappingId; + } } diff --git a/src/main/java/de/avatic/lcc/dto/configuration/rates/ContainerRateDTO.java b/src/main/java/de/avatic/lcc/dto/configuration/rates/ContainerRateDTO.java index cccbcab..486901f 100644 --- a/src/main/java/de/avatic/lcc/dto/configuration/rates/ContainerRateDTO.java +++ b/src/main/java/de/avatic/lcc/dto/configuration/rates/ContainerRateDTO.java @@ -12,6 +12,7 @@ public class ContainerRateDTO { private Integer id; private NodeDTO origin; + private NodeDTO destination; private TransportType type; @@ -21,6 +22,7 @@ public class ContainerRateDTO { @JsonProperty("lead_time") private Number leadTime; + @JsonProperty("validity_period") private ValidityPeriodDTO validityPeriod; public Integer getId() { diff --git a/src/main/java/de/avatic/lcc/dto/generic/LocationDTO.java b/src/main/java/de/avatic/lcc/dto/generic/LocationDTO.java index 02771de..3ecc6ab 100644 --- a/src/main/java/de/avatic/lcc/dto/generic/LocationDTO.java +++ b/src/main/java/de/avatic/lcc/dto/generic/LocationDTO.java @@ -1,5 +1,7 @@ package de.avatic.lcc.dto.generic; +import com.fasterxml.jackson.annotation.JsonFormat; + import java.util.Objects; /** diff --git a/src/main/java/de/avatic/lcc/dto/generic/NodeDTO.java b/src/main/java/de/avatic/lcc/dto/generic/NodeDTO.java index 677ccfc..5c95e3d 100644 --- a/src/main/java/de/avatic/lcc/dto/generic/NodeDTO.java +++ b/src/main/java/de/avatic/lcc/dto/generic/NodeDTO.java @@ -13,12 +13,16 @@ public class NodeDTO { private List types; private LocationDTO location; + @JsonProperty("external_mapping_id") + private String externalMappingId; + @JsonProperty("is_user_node") private Boolean isUserNode; @JsonProperty("is_deprecated") private Boolean isDeprecated; + @JsonProperty("is_user_node") public Boolean getUserNode() { return isUserNode; } @@ -83,4 +87,12 @@ public class NodeDTO { public void setLocation(LocationDTO location) { this.location = location; } + + public String getExternalMappingId() { + return externalMappingId; + } + + public void setExternalMappingId(String externalMappingId) { + this.externalMappingId = externalMappingId; + } } diff --git a/src/main/java/de/avatic/lcc/model/nodes/Node.java b/src/main/java/de/avatic/lcc/model/nodes/Node.java index 122103b..f0610c2 100644 --- a/src/main/java/de/avatic/lcc/model/nodes/Node.java +++ b/src/main/java/de/avatic/lcc/model/nodes/Node.java @@ -1,6 +1,7 @@ package de.avatic.lcc.model.nodes; import jakarta.validation.constraints.*; +import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.OffsetDateTime; @@ -9,6 +10,7 @@ import java.util.List; import java.util.Map; +@Validated public class Node { private Integer id; diff --git a/src/main/java/de/avatic/lcc/model/premises/PremiseListEntry.java b/src/main/java/de/avatic/lcc/model/premises/PremiseListEntry.java index 99f7de9..a1812aa 100644 --- a/src/main/java/de/avatic/lcc/model/premises/PremiseListEntry.java +++ b/src/main/java/de/avatic/lcc/model/premises/PremiseListEntry.java @@ -30,8 +30,17 @@ public class PremiseListEntry { private boolean supplierIsSource; private boolean supplierIsIntermediate; + private Boolean isDeprecated; + private Integer ownerId; + public Boolean getDeprecated() { + return isDeprecated; + } + + public void setDeprecated(Boolean deprecated) { + isDeprecated = deprecated; + } public BigDecimal getSupplierGeoLongitude() { return supplierGeoLongitude; diff --git a/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java b/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java index e67d0ad..883cfd5 100644 --- a/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java @@ -16,6 +16,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.*; +import java.util.stream.Collectors; @Repository public class MaterialRepository { @@ -146,6 +147,33 @@ public class MaterialRepository { } + /** + * Returns all IDs from the input list that don't exist in the material table + * @param ids List of integers to check + * @return List of integers that don't have corresponding rows in material table + */ + public List findMissingIds(List ids) { + if (ids == null || ids.isEmpty()) { + return List.of(); + } + + // Create placeholders for the IN clause + String placeholders = ids.stream() + .map(id -> "?") + .collect(Collectors.joining(",")); + + // Query to find existing IDs + String sql = "SELECT id FROM material WHERE id IN (" + placeholders + ")"; + + // Execute query and get existing IDs + Set existingIds = new HashSet<>(jdbcTemplate.queryForList(sql, Integer.class, ids.toArray())); + + // Return IDs that are in the input list but not in the database + return ids.stream() + .filter(id -> !existingIds.contains(id)) + .collect(Collectors.toList()); + } + private static class MaterialMapper implements RowMapper { @Override diff --git a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java index 2775f37..2701728 100644 --- a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java @@ -4,6 +4,7 @@ import de.avatic.lcc.dto.generic.NodeType; import de.avatic.lcc.model.nodes.Node; import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import de.avatic.lcc.repositories.pagination.SearchQueryResult; +import org.apache.commons.lang3.NotImplementedException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; @@ -31,9 +32,11 @@ public class NodeRepository { FROM node WHERE node.id = ?"""; - var node = jdbcTemplate.queryForObject(query, new NodeMapper(), id); + var node = jdbcTemplate.query(query, new NodeMapper(), id); - return Optional.ofNullable(node); + if (node.isEmpty()) return Optional.empty(); + + return Optional.ofNullable(node.getFirst()); } public List getByIds(List nodeIds) { @@ -42,9 +45,9 @@ public class NodeRepository { String query = """ SELECT * FROM node - WHERE node.id IN (?)"""; + WHERE node.id IN (""" + placeholders + ")"; - return jdbcTemplate.query(query, new NodeMapper(), nodeIds.toArray()); + return nodeIds.isEmpty() ? Collections.emptyList() : jdbcTemplate.query(query, new NodeMapper(), nodeIds.toArray()); } private List> getPredecessorsOf(Integer id) { @@ -54,8 +57,11 @@ public class NodeRepository { """; return jdbcTemplate.query(queryChains, (chainRs, rowNum) -> { + + Integer chainId = chainRs.getInt("id"); + String query = """ - SELECT entry.node_id AS predecessor , entry.sequence_number as sequence_number + SELECT entry.node_id AS predecessor_node_id , entry.sequence_number as sequence_number FROM node_predecessor_entry AS entry WHERE entry.node_predecessor_chain_id = ? ORDER BY entry.sequence_number"""; @@ -67,12 +73,10 @@ public class NodeRepository { } return predecessors; - }, id); + }, chainId); }, id); } - ; - private Collection getOutboundCountriesOf(Integer id) { String query = """ SELECT outbound_country_mapping.country_id @@ -104,13 +108,15 @@ public class NodeRepository { private String buildCountQuery(String filter, boolean excludeDeprecated) { StringBuilder queryBuilder = new StringBuilder(""" SELECT COUNT(*) - FROM WHERE 1=1"""); + FROM node + LEFT JOIN country ON country.id = node.country_id + WHERE 1=1"""); if (excludeDeprecated) { - queryBuilder.append(" AND is_deprecated = FALSE"); + queryBuilder.append(" AND node.is_deprecated = FALSE"); } if (filter != null) { - queryBuilder.append(" AND (name LIKE ? OR address LIKE ? OR country_iso_code LIKE ?)"); + queryBuilder.append(" AND (node.name LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)"); } return queryBuilder.toString(); @@ -118,20 +124,19 @@ public class NodeRepository { private String buildQuery(String filter, Boolean excludeDeprecated, SearchQueryPagination searchQueryPagination) { StringBuilder queryBuilder = new StringBuilder(""" - SELECT chain.id AS id, chain.name AS name, chain.address as address, chain.is_source as is_source, - chain.is_destination as is_destination, chain.is_intermediate as is_intermediate, chain.country_id as country_id, chain.predecessor_required as predecessor_required, - country.iso_code AS country_iso_code, country.region_code AS country_region_code, country.name AS country_name, country.is_deprecated AS country_is_deprecated - FROM chain - LEFT JOIN country ON country.id = chain.country_id + SELECT * + FROM node + LEFT JOIN country ON country.id = node.country_id + WHERE 1=1 """); if (excludeDeprecated) { - queryBuilder.append(" AND is_deprecated = FALSE"); + queryBuilder.append(" AND node.is_deprecated = FALSE"); } if (filter != null) { - queryBuilder.append(" AND (name LIKE ? OR address LIKE ? OR country_iso_code LIKE ?)"); + queryBuilder.append(" AND (node.name LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)"); } - queryBuilder.append(" ORDER BY id LIMIT ? OFFSET ?"); + queryBuilder.append(" ORDER BY node.id LIMIT ? OFFSET ?"); return queryBuilder.toString(); } @@ -142,21 +147,39 @@ public class NodeRepository { } @Transactional - public Optional update(Node chain) { + public Optional update(Node node) { + + throw new NotImplementedException("Update of nodes is not yet implemented!"); //TODO update predecessors and outbound_countries too //TODO implement correctly - //TODO if chain is updated set all linked RouteNodes to outdated! + //TODO if node is updated set all linked RouteNodes to outdated! - String query = "UPDATE node SET name = ?, address = ?, country_id = ?, is_source = ?, is_destination = ?, is_intermediate = ?, predecessor_required = ? WHERE id = ?"; - return Optional.ofNullable(jdbcTemplate.update(query, chain.getId()) == 0 ? null : chain.getId()); +// String query = "UPDATE node SET name = ?, address = ?, country_id = ?, is_source = ?, is_destination = ?, is_intermediate = ?, predecessor_required = ? WHERE id = ?"; +// +// var nodeId = jdbcTemplate.update(query, +// node.getName(), +// node.getAddress(), +// node.getCountryId(), +// node.getSource(), +// node.getDestination(), +// node.getIntermediate(), +// node.getPredecessorRequired(), +// node.getId()) == 0 ? null : node.getId(); +// +// return Optional.ofNullable(nodeId); } public List searchNode(String filter, int limit, NodeType nodeType, boolean excludeDeprecated) { - StringBuilder queryBuilder = new StringBuilder().append("SELECT * FROM chain WHERE (name LIKE ? OR address LIKE ?)"); + StringBuilder queryBuilder = new StringBuilder().append("SELECT * FROM node WHERE (name LIKE ? OR address LIKE ?)"); if (nodeType != null) { - queryBuilder.append(" AND node_type = ?"); + if (nodeType.equals(NodeType.SOURCE)) + queryBuilder.append(" AND is_source = true"); + if (nodeType.equals(NodeType.DESTINATION)) + queryBuilder.append(" AND is_destination = true"); + if (nodeType.equals(NodeType.INTERMEDIATE)) + queryBuilder.append(" AND is_intermediate = true"); } if (excludeDeprecated) { @@ -165,15 +188,12 @@ public class NodeRepository { queryBuilder.append(" LIMIT ?"); - if (nodeType != null) { - return jdbcTemplate.query(queryBuilder.toString(), new NodeMapper(), "%" + filter + "%", "%" + filter + "%", nodeType.name(), limit); - } return jdbcTemplate.query(queryBuilder.toString(), new NodeMapper(), "%" + filter + "%", "%" + filter + "%", limit); } public List listAllNodes(boolean onlySources) { - StringBuilder queryBuilder = new StringBuilder("SELECT * FROM chain"); + StringBuilder queryBuilder = new StringBuilder("SELECT * FROM node"); if (onlySources) { queryBuilder.append(" WHERE is_source = true"); } @@ -188,9 +208,9 @@ public class NodeRepository { FROM node WHERE node.external_mapping_id = ?"""; - var chain = jdbcTemplate.queryForObject(query, new NodeMapper(), mappingId); + var node = jdbcTemplate.queryForObject(query, new NodeMapper(), mappingId); - return Optional.ofNullable(chain); + return Optional.ofNullable(node); } @Transactional @@ -358,7 +378,7 @@ public class NodeRepository { var node = jdbcTemplate.query(query, new NodeMapper(), id); - if(node.isEmpty()) { + if (node.isEmpty()) { return Optional.empty(); } diff --git a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java index 34c7a64..477a181 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java @@ -91,19 +91,20 @@ public class DestinationRepository { public Integer insert(Destination destination) { KeyHolder keyHolder = new GeneratedKeyHolder(); - String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, rate_d2d, lead_time_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, country_id, rate_d2d, lead_time_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; jdbcTemplate.update(connection -> { var ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); ps.setInt(1, destination.getAnnualAmount()); ps.setInt(2, destination.getPremiseId()); ps.setInt(3, destination.getDestinationNodeId()); - ps.setBigDecimal(4, destination.getRateD2d()); - ps.setInt(5, destination.getLeadTimeD2d()); - ps.setBoolean(6, destination.getD2d()); - ps.setBigDecimal(7, destination.getRepackingCost()); - ps.setBigDecimal(8, destination.getHandlingCost()); - ps.setBigDecimal(9, destination.getDisposalCost()); + ps.setInt(4, destination.getCountryId()); + ps.setBigDecimal(5, destination.getRateD2d()); + ps.setInt(6, destination.getLeadTimeD2d()); + ps.setBoolean(7, destination.getD2d()); + ps.setBigDecimal(8, destination.getRepackingCost()); + ps.setBigDecimal(9, destination.getHandlingCost()); + ps.setBigDecimal(10, destination.getDisposalCost()); return ps; }, keyHolder); diff --git a/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java index a8bc2d9..578212d 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java @@ -11,6 +11,7 @@ import de.avatic.lcc.model.utils.DimensionUnit; import de.avatic.lcc.model.utils.WeightUnit; import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import de.avatic.lcc.repositories.pagination.SearchQueryResult; +import de.avatic.lcc.util.exception.internalerror.DatabaseException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -105,7 +106,7 @@ public class PremiseRepository { return new Object[]{ userId, wildcardFilter, wildcardFilter, wildcardFilter, wildcardFilter, - wildcardFilter, wildcardFilter, wildcardFilter, + wildcardFilter, wildcardFilter, pagination.getLimit(), pagination.getOffset() }; } @@ -113,7 +114,7 @@ public class PremiseRepository { private Object[] createFilterParamsForCount(Integer userId, String wildcardFilter) { return new Object[]{ userId, - wildcardFilter, wildcardFilter, wildcardFilter, wildcardFilter, + wildcardFilter, wildcardFilter, wildcardFilter, wildcardFilter, wildcardFilter, wildcardFilter }; } @@ -159,9 +160,9 @@ public class PremiseRepository { } @Transactional - public void updatePackaging(List premiseIds, Integer userId, PackagingDimension dimensionEntity, Boolean stackable, Boolean mixable) { + public void updatePackaging(List premiseIds, Integer userId, PackagingDimension hu, PackagingDimension shu, Boolean stackable, Boolean mixable) { - if (premiseIds == null || premiseIds.isEmpty() || userId == null || dimensionEntity == null) { + if (premiseIds == null || premiseIds.isEmpty() || userId == null || hu == null) { return; } @@ -169,13 +170,54 @@ public class PremiseRepository { boolean isMixable = mixable != null ? mixable : false; MapSqlParameterSource params = new MapSqlParameterSource(); - params.addValue("length", dimensionEntity.getLength()); - params.addValue("height", dimensionEntity.getHeight()); - params.addValue("width", dimensionEntity.getWidth()); - params.addValue("weight", dimensionEntity.getWeight()); - params.addValue("dimensionUnit", dimensionEntity.getDimensionUnit().name()); - params.addValue("weightUnit", dimensionEntity.getWeightUnit().name()); - params.addValue("unitCount", dimensionEntity.getContentUnitCount()); + params.addValue("length", hu.getLength()); + params.addValue("height", hu.getHeight()); + params.addValue("width", hu.getWidth()); + params.addValue("weight", hu.getWeight()); + params.addValue("dimensionUnit", hu.getDimensionUnit().name()); + params.addValue("weightUnit", hu.getWeightUnit().name()); + params.addValue("unitCount", hu.getContentUnitCount()*shu.getContentUnitCount()); + params.addValue("stackable", isStackable); + params.addValue("mixable", isMixable); + params.addValue("userId", userId); + params.addValue("premiseIds", premiseIds); + + String sql = """ + UPDATE premise + SET individual_hu_length = :length, + individual_hu_height = :height, + individual_hu_width = :width, + individual_hu_weight = :weight, + hu_displayed_dimension_unit = :dimensionUnit, + hu_displayed_weight_unit = :weightUnit, + hu_unit_count = :unitCount, + hu_stackable = :stackable, + hu_mixable = :mixable + WHERE user_id = :userId AND id IN (:premiseIds) + """; + + namedParameterJdbcTemplate.update(sql, params); + + } + + @Transactional + public void updatePackaging(List premiseIds, Integer userId, PackagingDimension hu, Boolean stackable, Boolean mixable) { + + if (premiseIds == null || premiseIds.isEmpty() || userId == null || hu == null) { + return; + } + + boolean isStackable = stackable != null ? stackable : false; + boolean isMixable = mixable != null ? mixable : false; + + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("length", hu.getLength()); + params.addValue("height", hu.getHeight()); + params.addValue("width", hu.getWidth()); + params.addValue("weight", hu.getWeight()); + params.addValue("dimensionUnit", hu.getDimensionUnit().name()); + params.addValue("weightUnit", hu.getWeightUnit().name()); + params.addValue("unitCount", hu.getContentUnitCount()); params.addValue("stackable", isStackable); params.addValue("mixable", isMixable); params.addValue("userId", userId); @@ -231,7 +273,7 @@ public class PremiseRepository { String query = """ UPDATE premise - SET price = ?, + SET material_cost = ?, is_fca_enabled = ?, oversea_share = ? WHERE user_id = ? AND id IN (""" + placeholders + ")"; @@ -246,12 +288,19 @@ public class PremiseRepository { ps.setInt(4, userId); for (int parameterIndex = 0; parameterIndex < premiseIds.size(); parameterIndex++) { - ps.setInt(parameterIndex + 6, premiseIds.get(parameterIndex)); + ps.setInt(parameterIndex + 5, premiseIds.get(parameterIndex)); } } ); } + @Transactional + public void updateTariffRate(Integer premiseId, Number tariffRate) { + String sql = "UPDATE premise SET tariff_rate = ? WHERE id = ?"; + + jdbcTemplate.update(sql, tariffRate, premiseId); + } + /** * Retrieves all draft premises that would conflict with updating the material or supplier for a given premise. @@ -263,14 +312,14 @@ public class PremiseRepository { * @param userId The ID of the user who owns the premises. * @param premiseId The ID of the premise being checked; this premise will be excluded from the results. * @param materialId The material ID to verify for potential conflicts. - * @param supplierId The supplier chain ID to verify for potential conflicts. + * @param supplierId The supplier node ID to verify for potential conflicts. * @return A list of premises in the DRAFT state with the same material and supplier combination (excluding the specified premise). * @throws IllegalArgumentException If any of the provided parameters are null. */ @Transactional(readOnly = true) public List getCollidingPremisesOnChange(Integer userId, Integer premiseId, Integer materialId, Integer supplierId) { - if( premiseId == null || materialId == null || supplierId == null ) + if (premiseId == null || materialId == null || supplierId == null) return Collections.emptyList(); String sql = "SELECT * FROM premise " + @@ -285,7 +334,7 @@ public class PremiseRepository { @Transactional public void deletePremisesById(List premiseIds) { - if(premiseIds == null || premiseIds.isEmpty()) return; + if (premiseIds == null || premiseIds.isEmpty()) return; String placeholders = String.join(",", Collections.nCopies(premiseIds.size(), "?")); String query = """ @@ -295,19 +344,13 @@ public class PremiseRepository { jdbcTemplate.update(query, premiseIds.toArray()); } - @Transactional - public void updateTariffRate(Integer premiseId, Number tariffRate) { - String sql = "UPDATE premise SET tariff_rate = ? WHERE id = ?"; - - jdbcTemplate.update(sql, tariffRate, premiseId); - } @Transactional public void setSupplierId(List premiseId, Node supplier, boolean userSupplierNode) { String placeholders = String.join(",", Collections.nCopies(premiseId.size(), "?")); - String sql = "UPDATE premise SET supplier_node_id = ?, user_supplier_node_id = ?, geo_lat = ?, geo_lng = ?, country_id = ? WHERE id IN ("+placeholders+")"; + String sql = "UPDATE premise SET supplier_node_id = ?, user_supplier_node_id = ?, geo_lat = ?, geo_lng = ?, country_id = ? WHERE id IN (" + placeholders + ")"; - if(userSupplierNode) { + if (userSupplierNode) { jdbcTemplate.update(sql, 0, supplier.getId(), supplier.getGeoLat(), supplier.getGeoLng(), supplier.getCountryId(), premiseId.toArray()); } else { jdbcTemplate.update(sql, supplier.getId(), 0, supplier.getGeoLat(), supplier.getGeoLng(), supplier.getCountryId(), premiseId.toArray()); @@ -317,11 +360,32 @@ public class PremiseRepository { @Transactional public void setMaterialId(List premiseId, Integer materialId) { String placeholders = String.join(",", Collections.nCopies(premiseId.size(), "?")); - String sql = "UPDATE premise SET material_id = ? WHERE id IN ("+placeholders+")"; + String sql = "UPDATE premise SET material_id = ? WHERE id IN (" + placeholders + ")"; jdbcTemplate.update(sql, materialId, premiseId.toArray()); } + @Transactional + public List findByMaterialIdAndSupplierId(Integer materialId, Integer supplierId, Integer userSupplierId, Integer userId) { + + if((supplierId == null && userSupplierId == null) || (supplierId != null && userSupplierId != null)) + throw new DatabaseException("Either supplierId or userSupplierId must be null"); + + String query = """ + SELECT * FROM premise + WHERE material_id = ? AND (user_id = ? + """; + + if(userSupplierId != null) + query += ") AND user_supplier_node_id = ?"; + else query += "OR state = 'COMPLETED') AND supplier_node_id = ?"; + + + query += " ORDER BY updated_at DESC"; + + return jdbcTemplate.query(query, new PremiseMapper(), materialId, userId, userSupplierId != null ? userSupplierId : supplierId); + } + @Transactional public List getPremisesByMaterialIdsAndSupplierIds(List materialIds, List supplierIds, List userSupplierIds, Integer userId, boolean draftsOnly) { @@ -336,7 +400,7 @@ public class PremiseRepository { WHERE 1=1 """); - if(draftsOnly) + if (draftsOnly) query.append(" AND user_id = ? AND state = ?"); // Append conditions for IDs @@ -358,7 +422,7 @@ public class PremiseRepository { // Combine parameters for prepared statement var params = new ArrayList<>(); - if(draftsOnly) { + if (draftsOnly) { params.add(userId); params.add(PremiseState.DRAFT.name()); } @@ -371,20 +435,23 @@ public class PremiseRepository { } @Transactional - public Integer insert(Integer materialId, Integer supplierId, Integer userSupplierId, Integer userId) { + public Integer insert(Integer materialId, Integer supplierId, Integer userSupplierId, BigDecimal geoLat, BigDecimal geoLng, Integer countryId, Integer userId) { KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement( - "INSERT INTO premise (material_id, supplier_node_id, user_supplier_node_id, user_id, state, created_at, updated_at)" + - " VALUES (?, ?, ?, ?, 'DRAFT', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", + "INSERT INTO premise (material_id, supplier_node_id, user_supplier_node_id, user_id, state, created_at, updated_at, geo_lat, geo_lng, country_id)" + + " VALUES (?, ?, ?, ?, 'DRAFT', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS); ps.setInt(1, materialId); ps.setObject(2, supplierId); ps.setObject(3, userSupplierId); ps.setInt(4, userId); + ps.setBigDecimal(5, geoLat); + ps.setBigDecimal(6, geoLng); + ps.setInt(7, countryId); return ps; }, keyHolder); @@ -413,25 +480,39 @@ public class PremiseRepository { } @Transactional - public List findAssociatedUserSuppliers(List materialIds) { + public List findAssociatedUserSuppliers(List materialIds, Integer userId) { if (materialIds == null || materialIds.isEmpty()) { return Collections.emptyList(); } + List params = new ArrayList<>(materialIds); + params.add(userId); + String placeholders = String.join(",", Collections.nCopies(materialIds.size(), "?")); String query = """ - SELECT DISTINCT user_supplier_node_id - FROM premise - WHERE material_id IN (""" + placeholders + ")"; + SELECT DISTINCT p.user_supplier_node_id AS user_supplier_node_id + FROM premise p + JOIN sys_user_node sun ON p.user_supplier_node_id = sun.id + WHERE p.material_id IN (""" + placeholders + ") AND sun.user_id = ? "; return jdbcTemplate.query( query, (rs, rowNum) -> rs.getInt("user_supplier_node_id"), - materialIds.toArray() + params.toArray() ); } + public void setPackagingId(Integer id, Integer packagingId) { + String query = "UPDATE premise SET packaging_id = ? WHERE id = ?"; + + var affectedRows = jdbcTemplate.update(query, packagingId, id); + + if(affectedRows == 0) + throw new DatabaseException("No premise found with id " + id); + } + + /** * Encapsulates SQL query building logic */ @@ -439,13 +520,13 @@ public class PremiseRepository { private static final String BASE_JOIN_QUERY = """ FROM premise AS p LEFT JOIN material as m ON p.material_id = m.id - LEFT JOIN chain as n ON p.supplier_node_id = n.id + LEFT JOIN node as n ON p.supplier_node_id = n.id LEFT JOIN sys_user_node as user_n ON p.user_supplier_node_id = user_n.id - WHERE p.userId = ?"""; + WHERE p.user_id = ?"""; private static final String FILTER_CONDITION = - " AND (n.name LIKE ? OR n.external_mapping_id LIKE ? OR n.note LIKE ? OR " + - "user_n.name LIKE ? OR m.name LIKE ? OR m.description LIKE ? OR m.part_number LIKE ?)"; + " AND (n.name LIKE ? OR n.external_mapping_id LIKE ? OR " + + "user_n.name LIKE ? OR m.name LIKE ? OR m.name LIKE ? OR m.part_number LIKE ?)"; private String filter; private Boolean deleted; @@ -481,7 +562,19 @@ public class PremiseRepository { public String buildSelectQuery() { StringBuilder queryBuilder = new StringBuilder(); - queryBuilder.append("SELECT * ").append(BASE_JOIN_QUERY); + queryBuilder.append(""" + SELECT p.id as 'p.id', p.state as 'p.state', p.user_id as 'p.user_id', + p.created_at as 'p.created_at', p.updated_at as 'p.updated_at', + p.supplier_node_id as 'p.supplier_node_id', p.user_supplier_node_id as 'p.user_supplier_node_id', + m.id as 'm.id', m.name as 'm.name', m.is_deprecated as 'm.is_deprecated', + m.hs_code as 'm.hs_code', m.part_number as 'm.part_number', + n.id as 'n.id', n.name as 'n.name', n.address as 'n.address', + n.country_id as 'n.country_id', n.geo_lat as 'n.geo_lat', n.geo_lng as 'n.geo_lng', + n.is_destination as 'n.is_destination', n.is_intermediate as 'n.is_intermediate', n.is_source as 'n.is_source', + n.is_deprecated as 'n.is_deprecated', + user_n.id as 'user_n.id', user_n.name as 'user_n.name', user_n.address as 'user_n.address', + user_n.country_id as 'user_n.country_id', user_n.geo_lat as 'user_n.geo_lat', user_n.geo_lng as 'user_n.geo_lng' + """).append(BASE_JOIN_QUERY); appendConditions(queryBuilder); queryBuilder.append(" ORDER BY p.updated_at DESC"); queryBuilder.append(" LIMIT ? OFFSET ?"); @@ -493,14 +586,39 @@ public class PremiseRepository { queryBuilder.append(FILTER_CONDITION); } - appendBooleanCondition(queryBuilder, deleted, "p.deleted"); - appendBooleanCondition(queryBuilder, archived, "p.archived"); - appendBooleanCondition(queryBuilder, done, "p.done"); + if (deleted != null && deleted || archived != null && archived || done != null && done) { + boolean concat = false; + + queryBuilder.append(" AND ("); + + if (deleted != null && deleted) { + queryBuilder.append(" p.state").append(" = 'DELETED'"); + concat = true; + } + + if (archived != null && archived) { + if (concat) + queryBuilder.append(" OR "); + queryBuilder.append(" p.state").append(" = 'ARCHIVED'"); + concat = true; + } + + if (done != null && done) { + if (concat) + queryBuilder.append(" OR "); + queryBuilder.append(" p.state").append(" = 'COMPLETED'"); + concat = true; + } + + queryBuilder.append(")"); + + } + } private void appendBooleanCondition(StringBuilder queryBuilder, Boolean condition, String field) { if (condition != null && condition) { - queryBuilder.append(" AND ").append(field).append(" = TRUE"); + queryBuilder.append(" OR ").append(field).append(" = TRUE"); } } } @@ -516,7 +634,7 @@ public class PremiseRepository { // Map material entity.setMaterial(mapMaterial(rs)); - // Map supplier (either regular chain or user chain) + // Map supplier (either regular node or user node) mapSupplierProperties(entity, rs); return entity; @@ -544,25 +662,34 @@ public class PremiseRepository { private void mapSupplierProperties(PremiseListEntry entity, ResultSet rs) throws SQLException { if (rs.getInt("p.supplier_node_id") != 0) { - mapSupplier(entity, rs, "n"); + mapSupplier(false, entity, rs, "n"); } else if (rs.getInt("p.user_supplier_node_id") != 0) { - mapSupplier(entity, rs, "user_n"); + mapSupplier(true, entity, rs, "user_n"); entity.setUserSupplier(true); } } - private void mapSupplier(PremiseListEntry entity, ResultSet rs, String tablePrefix) throws SQLException { + private void mapSupplier(boolean isUserNode, PremiseListEntry entity, ResultSet rs, String tablePrefix) throws SQLException { entity.setSupplierId(rs.getInt(tablePrefix + ".id")); entity.setSupplierName(rs.getString(tablePrefix + ".name")); entity.setSupplierAddress(rs.getString(tablePrefix + ".address")); entity.setSupplierCountryId(rs.getInt(tablePrefix + ".country_id")); - entity.setSupplierGeoLatitude(rs.getBigDecimal(tablePrefix + ".geo_latitude")); - entity.setSupplierGeoLongitude(rs.getBigDecimal(tablePrefix + ".geo_longitude")); + entity.setSupplierGeoLatitude(rs.getBigDecimal(tablePrefix + ".geo_lat")); + entity.setSupplierGeoLongitude(rs.getBigDecimal(tablePrefix + ".geo_lng")); + + if (!isUserNode) { + entity.setSupplierIsDestination(rs.getBoolean(tablePrefix + ".is_destination")); + entity.setSupplierIsIntermediate(rs.getBoolean(tablePrefix + ".is_intermediate")); + entity.setSupplierIsSource(rs.getBoolean(tablePrefix + ".is_source")); + entity.setDeprecated(rs.getBoolean(tablePrefix + ".is_deprecated")); + } else { + entity.setSupplierIsDestination(false); + entity.setSupplierIsIntermediate(false); + entity.setSupplierIsSource(true); + entity.setDeprecated(false); + } - entity.setSupplierIsDestination(rs.getBoolean(tablePrefix + ".is_destination")); - entity.setSupplierIsIntermediate(rs.getBoolean(tablePrefix + ".is_intermediate")); - entity.setSupplierIsSource(rs.getBoolean(tablePrefix + ".is_source")); } } @@ -574,36 +701,78 @@ public class PremiseRepository { entity.setId(rs.getInt("id")); entity.setPackagingId(rs.getInt("packaging_id")); + if(rs.wasNull()) + entity.setPackagingId(null); + entity.setMaterialId(rs.getInt("material_id")); + if(rs.wasNull()) + entity.setMaterialId(null); + entity.setSupplierNodeId(rs.getInt("supplier_node_id")); + if(rs.wasNull()) + entity.setSupplierNodeId(null); + entity.setUserSupplierNodeId(rs.getInt("user_supplier_node_id")); + if(rs.wasNull()) + entity.setUserSupplierNodeId(null); entity.setLocation(new Location(rs.getBigDecimal("geo_lng").doubleValue(), rs.getBigDecimal("geo_lat").doubleValue())); + entity.setCountryId(rs.getInt("country_id")); + if(rs.wasNull()) + entity.setCountryId(null); entity.setUserId(rs.getInt("user_id")); + if(rs.wasNull()) + entity.setUserId(null); + entity.setMaterialCost(rs.getBigDecimal("material_cost")); entity.setHsCode(rs.getString("hs_code")); + entity.setCustomRate(rs.getBigDecimal("tariff_rate")); + entity.setFcaEnabled(rs.getBoolean("is_fca_enabled")); + if(rs.wasNull()) + entity.setFcaEnabled(null); + entity.setOverseaShare(rs.getBigDecimal("oversea_share")); entity.setIndividualHuHeight(rs.getInt("individual_hu_height")); + if(rs.wasNull()) + entity.setIndividualHuHeight(null); + entity.setIndividualHuWidth(rs.getInt("individual_hu_width")); + if(rs.wasNull()) + entity.setIndividualHuWidth(null); + entity.setIndividualHuLength(rs.getInt("individual_hu_length")); + if(rs.wasNull()) + entity.setIndividualHuLength(null); + entity.setIndividualHuWeight(rs.getInt("individual_hu_weight")); + if(rs.wasNull()) + entity.setIndividualHuWeight(null); + entity.setHuDisplayedDimensionUnit(DimensionUnit.valueOf(rs.getString("hu_displayed_dimension_unit"))); entity.setHuDisplayedWeightUnit(WeightUnit.valueOf(rs.getString("hu_displayed_weight_unit"))); + entity.setHuStackable(rs.getBoolean("hu_stackable")); + if(rs.wasNull()) + entity.setHuStackable(null); + entity.setHuMixable(rs.getBoolean("hu_mixable")); + if(rs.wasNull()) + entity.setHuMixable(null); + entity.setHuUnitCount(rs.getInt("hu_unit_count")); + if(rs.wasNull()) + entity.setHuUnitCount(null); entity.setState(PremiseState.valueOf(rs.getString("state"))); entity.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime()); entity.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); - return entity; } } diff --git a/src/main/java/de/avatic/lcc/repositories/premise/RouteNodeRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/RouteNodeRepository.java index 68f87f1..d22dadc 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/RouteNodeRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/RouteNodeRepository.java @@ -48,8 +48,10 @@ public class RouteNodeRepository { } public Integer insert(RouteNode node) { - String sql = "INSERT INTO premise_route_node (name, address, geo_lat, geo_lng, is_destination, is_intermediate, " + - "is_source, node_id, user_node_id, is_outdated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + String sql = """ + INSERT INTO premise_route_node (name, address, geo_lat, geo_lng, is_destination, is_intermediate, + is_source, node_id, user_node_id, is_outdated, country_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { @@ -64,6 +66,7 @@ public class RouteNodeRepository { ps.setObject(8, node.getNodeId(), java.sql.Types.INTEGER); ps.setObject(9, node.getUserNodeId(), java.sql.Types.INTEGER); ps.setBoolean(10, Boolean.TRUE.equals(node.getOutdated())); + ps.setObject(11, node.getCountryId(), java.sql.Types.INTEGER); return ps; }, keyHolder); @@ -71,7 +74,9 @@ public class RouteNodeRepository { } public Optional getFromNodeBySectionId(Integer id) { - String sql = "SELECT * FROM premise_route_section LEFT JOIN premise_route_node ON premise_route_node.id = premise_route_section.from_route_node_id WHERE premise_route_section.id = ?"; + String sql = """ + SELECT * FROM premise_route_section LEFT JOIN premise_route_node ON premise_route_node.id = premise_route_section.from_route_node_id WHERE premise_route_section.id = ? + """; var node = jdbcTemplate.query(sql, new RouteNodeMapper(), id); @@ -112,6 +117,8 @@ public class RouteNodeRepository { entity.setNodeId(rs.getInt("node_id")); entity.setUserNodeId(rs.getInt("user_node_id")); + entity.setCountryId(rs.getInt("country_id")); + entity.setOutdated(rs.getBoolean("is_outdated")); return entity; diff --git a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java index 315e1e5..9b24a11 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ContainerRateRepository.java @@ -34,9 +34,15 @@ public class ContainerRateRepository { return new SearchQueryResult<>(jdbcTemplate.query(query, new ContainerRateMapper(), periodId, pagination.getLimit(), pagination.getOffset()), pagination.getPage(), totalCount, pagination.getLimit()); } - public ContainerRate getById(Integer id) { + public Optional getById(Integer id) { String query = "SELECT * FROM container_rate WHERE id = ?"; - return jdbcTemplate.queryForObject(query, new ContainerRateMapper(), id); + + var rate = jdbcTemplate.query(query, new ContainerRateMapper(), id); + + if(rate.isEmpty()) + return Optional.empty(); + + return Optional.of(rate.getFirst()); } public List listAllRatesByPeriodId(Integer periodId) { diff --git a/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java index 6ca6f5d..1ecd8b1 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/MatrixRateRepository.java @@ -56,7 +56,7 @@ public class MatrixRateRepository { @Transactional public SearchQueryResult listRatesByPeriodId(SearchQueryPagination pagination, Integer periodId) { String query = "SELECT * FROM country_matrix_rate WHERE validity_period_id = ? ORDER BY id LIMIT ? OFFSET ?"; - var totalCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM country_matrix_rate WHERE validity_period_id = ?", Integer.class); + var totalCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM country_matrix_rate WHERE validity_period_id = ?", Integer.class, periodId); return new SearchQueryResult<>(jdbcTemplate.query(query, new MatrixRateMapper(), periodId, pagination.getLimit(), pagination.getOffset()), pagination.getPage(), totalCount, pagination.getLimit()); } @@ -108,8 +108,8 @@ public class MatrixRateRepository { entity.setId(rs.getInt("id")); entity.setRate(rs.getBigDecimal("rate")); - entity.setFromCountry(rs.getInt("from_country")); - entity.setToCountry(rs.getInt("to_country")); + entity.setFromCountry(rs.getInt("from_country_id")); + entity.setToCountry(rs.getInt("to_country_id")); return entity; } diff --git a/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java b/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java index d839e7a..eb30a90 100644 --- a/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/rates/ValidityPeriodRepository.java @@ -68,8 +68,9 @@ public class ValidityPeriodRepository { * @param id the ID of the validity period to invalidate. */ @Transactional - public void invalidateById(Integer id) { - jdbcTemplate.update("UPDATE validity_period SET state = ? WHERE id = ? AND state = ? ", ValidityPeriodState.INVALID.name(), id, ValidityPeriodState.EXPIRED.name()); + public boolean invalidateById(Integer id) { + var affectedRows = jdbcTemplate.update("UPDATE validity_period SET state = ? WHERE id = ? AND state = ? ", ValidityPeriodState.INVALID.name(), id, ValidityPeriodState.EXPIRED.name()); + return affectedRows > 0; } /** @@ -95,8 +96,8 @@ public class ValidityPeriodRepository { * * @return the ID of the valid {@link ValidityPeriod}. */ - public Integer getValidPeriodId() { - return getValidPeriod().getId(); + public Optional getValidPeriodId() { + return getValidPeriod().map(ValidityPeriod::getId); } /** @@ -104,9 +105,14 @@ public class ValidityPeriodRepository { * * @return the {@link ValidityPeriod} in the {@code VALID} state. */ - public ValidityPeriod getValidPeriod() { + public Optional getValidPeriod() { String query = "SELECT * FROM validity_period WHERE state = ?"; - return jdbcTemplate.queryForObject(query, new ValidityPeriodMapper(), ValidityPeriodState.VALID.name()); + var period = jdbcTemplate.query(query, new ValidityPeriodMapper(), ValidityPeriodState.VALID.name()); + + if(period.isEmpty()) + return Optional.empty(); + + return Optional.of(period.getFirst()); } /** @@ -157,7 +163,7 @@ public class ValidityPeriodRepository { /* set current to expired */ jdbcTemplate.update("UPDATE validity_period SET state = ?, end_date = ? WHERE state = ? ", ValidityPeriodState.EXPIRED.name(), currentTimestamp, ValidityPeriodState.VALID.name()); - jdbcTemplate.update("UPDATE validity_period SET state = ?, start_date = ? WHERE id = ? AND state = ? ", ValidityPeriodState.VALID.name(), currentTimestamp, ValidityPeriodState.DRAFT.name()); + jdbcTemplate.update("UPDATE validity_period SET state = ?, start_date = ? WHERE state = ? ", ValidityPeriodState.VALID.name(), currentTimestamp, ValidityPeriodState.DRAFT.name()); } @Transactional diff --git a/src/main/java/de/avatic/lcc/repositories/users/UserNodeRepository.java b/src/main/java/de/avatic/lcc/repositories/users/UserNodeRepository.java index a2c9809..1efef9b 100644 --- a/src/main/java/de/avatic/lcc/repositories/users/UserNodeRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/users/UserNodeRepository.java @@ -1,6 +1,8 @@ package de.avatic.lcc.repositories.users; import de.avatic.lcc.model.nodes.Node; +import de.avatic.lcc.repositories.NodeRepository; +import de.avatic.lcc.util.exception.internalerror.DatabaseException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; @@ -8,6 +10,7 @@ import org.springframework.stereotype.Repository; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -35,8 +38,26 @@ public class UserNodeRepository { return jdbcTemplate.query(queryBuilder.toString(), new NodeMapper(), userId, '%' + filter + '%', '%' + filter + '%', limit); } - public void add(Node node) { - // todo insert user node + public void add(Integer userId, Node node) { + String sql = """ + INSERT INTO sys_user_node ( + name, address, geo_lat, geo_lng, + is_deprecated, country_id, user_id + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """; + + var affectedRows = jdbcTemplate.update(sql, + node.getName(), + node.getAddress(), + node.getGeoLat(), + node.getGeoLng(), + node.getDeprecated(), + node.getCountryId(), + userId + ); + + if(affectedRows != 1) + throw new DatabaseException("Could not add node to user"); } public Optional getById(Integer id) { @@ -65,6 +86,30 @@ public class UserNodeRepository { } + public Collection getByIds(List nodeIds) { + String placeholders = String.join(",", Collections.nCopies(nodeIds.size(), "?")); + String query = """ + SELECT * + FROM sys_user_node + WHERE sys_user_node.id IN (""" + placeholders + ")"; + + return nodeIds.isEmpty() ? Collections.emptyList() : jdbcTemplate.query(query, new NodeMapper(), nodeIds.toArray()); + } + + public Optional getOwnerById(Integer userSupplierId) { + + String query = """ + SELECT user_id FROM sys_user_node WHERE id = ?; + """; + + var ids = jdbcTemplate.queryForList(query, Integer.class, userSupplierId); + + if(ids.isEmpty()) + return Optional.empty(); + + return Optional.of(ids.getFirst()); + } + private static class NodeMapper implements RowMapper { @Override @@ -80,6 +125,10 @@ public class UserNodeRepository { node.setDeprecated(rs.getBoolean("is_deprecated")); node.setCountryId(rs.getInt("country_id")); + node.setDestination(false); + node.setIntermediate(false); + node.setSource(true); + return node; } diff --git a/src/main/java/de/avatic/lcc/service/CustomApiService.java b/src/main/java/de/avatic/lcc/service/CustomApiService.java index 5826e39..676f17f 100644 --- a/src/main/java/de/avatic/lcc/service/CustomApiService.java +++ b/src/main/java/de/avatic/lcc/service/CustomApiService.java @@ -2,10 +2,12 @@ package de.avatic.lcc.service; import org.springframework.stereotype.Service; +import java.math.BigDecimal; + @Service public class CustomApiService { - public Double getTariffRate(String hsCode, Integer countryId) { - return Double.valueOf(3); + public BigDecimal getTariffRate(String hsCode, Integer countryId) { + return BigDecimal.valueOf(3); //TODO implement me } diff --git a/src/main/java/de/avatic/lcc/service/access/ContainerRateService.java b/src/main/java/de/avatic/lcc/service/access/ContainerRateService.java index a0e188b..7c4b002 100644 --- a/src/main/java/de/avatic/lcc/service/access/ContainerRateService.java +++ b/src/main/java/de/avatic/lcc/service/access/ContainerRateService.java @@ -11,6 +11,8 @@ import de.avatic.lcc.repositories.rates.ContainerRateRepository; import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; import de.avatic.lcc.service.transformer.generic.NodeTransformer; import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer; +import de.avatic.lcc.util.exception.badrequest.NotFoundException; +import de.avatic.lcc.util.exception.base.InternalErrorException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -80,8 +82,8 @@ public class ContainerRateService { */ @Transactional public SearchQueryResult listRates(int limit, int page) { - Integer id = validityPeriodRepository.getValidPeriodId(); - return SearchQueryResult.map(containerRateRepository.listRatesByPeriodId(new SearchQueryPagination(limit, page), id), this::toContainerRateDTO); + var data = validityPeriodRepository.getValidPeriodId().map(id -> containerRateRepository.listRatesByPeriodId(new SearchQueryPagination(page, limit), id)).orElseThrow(() -> new InternalErrorException("No validity period that is VALID")); + return SearchQueryResult.map(data, this::toContainerRateDTO); } /** @@ -92,7 +94,7 @@ public class ContainerRateService { */ @Transactional public ContainerRateDTO getContainerRate(Integer id) { - return toContainerRateDTO(containerRateRepository.getById(id)); + return toContainerRateDTO(containerRateRepository.getById(id).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.CONTAINER_RATE, "id", String.valueOf(id)))); } /** diff --git a/src/main/java/de/avatic/lcc/service/access/MatrixRateService.java b/src/main/java/de/avatic/lcc/service/access/MatrixRateService.java index f5d5cfb..37b4d03 100644 --- a/src/main/java/de/avatic/lcc/service/access/MatrixRateService.java +++ b/src/main/java/de/avatic/lcc/service/access/MatrixRateService.java @@ -2,6 +2,7 @@ package de.avatic.lcc.service.access; import de.avatic.lcc.dto.configuration.matrixrates.MatrixRateDTO; import de.avatic.lcc.model.rates.MatrixRate; +import de.avatic.lcc.model.rates.ValidityPeriod; import de.avatic.lcc.repositories.NodeRepository; import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import de.avatic.lcc.repositories.pagination.SearchQueryResult; @@ -62,7 +63,7 @@ public class MatrixRateService { if (null == periodId) return listRates(limit, page); - return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(new SearchQueryPagination(limit, page), periodId), this::toMatrixRateDTO); + return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(new SearchQueryPagination(page, limit), periodId), this::toMatrixRateDTO); } /** @@ -74,8 +75,8 @@ public class MatrixRateService { */ @Transactional public SearchQueryResult listRates(int limit, int page) { - Integer id = validityPeriodRepository.getValidPeriodId(); - return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(new SearchQueryPagination(limit, page), id), this::toMatrixRateDTO); + Integer id = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new IllegalStateException("No valid period found that is VALID")); + return SearchQueryResult.map(matrixRateRepository.listRatesByPeriodId(new SearchQueryPagination(page, limit), id), this::toMatrixRateDTO); } /** diff --git a/src/main/java/de/avatic/lcc/service/access/NodeService.java b/src/main/java/de/avatic/lcc/service/access/NodeService.java index ab5613c..438db3e 100644 --- a/src/main/java/de/avatic/lcc/service/access/NodeService.java +++ b/src/main/java/de/avatic/lcc/service/access/NodeService.java @@ -13,6 +13,7 @@ import de.avatic.lcc.service.transformer.nodes.NodeUpdateDTOTransformer; import de.avatic.lcc.service.transformer.nodes.NodeDetailTransformer; import de.avatic.lcc.service.transformer.generic.NodeTransformer; import de.avatic.lcc.util.exception.badrequest.NodeNotFoundException; +import de.avatic.lcc.util.exception.badrequest.NotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -30,15 +31,13 @@ public class NodeService { private final NodeDetailTransformer nodeDetailTransformer; private final NodeUpdateDTOTransformer nodeUpdateDTOTransformer; private final UserNodeRepository userNodeRepository; - private final CountryRepository countryRepository; - public NodeService(NodeRepository nodeRepository, NodeTransformer nodeTransformer, NodeDetailTransformer nodeDetailTransformer, NodeUpdateDTOTransformer nodeUpdateDTOTransformer, UserNodeRepository userNodeRepository, CountryRepository countryRepository) { + public NodeService(NodeRepository nodeRepository, NodeTransformer nodeTransformer, NodeDetailTransformer nodeDetailTransformer, NodeUpdateDTOTransformer nodeUpdateDTOTransformer, UserNodeRepository userNodeRepository) { this.nodeRepository = nodeRepository; this.nodeTransformer = nodeTransformer; this.nodeDetailTransformer = nodeDetailTransformer; this.nodeUpdateDTOTransformer = nodeUpdateDTOTransformer; this.userNodeRepository = userNodeRepository; - this.countryRepository = countryRepository; } @@ -74,7 +73,7 @@ public class NodeService { * @throws NodeNotFoundException if no node is found with the specified ID. */ public NodeDetailDTO getNode(Integer id) { - return nodeDetailTransformer.toNodeDetailDTO(nodeRepository.getById(id).orElseThrow(() -> new NodeNotFoundException(id))); + return nodeDetailTransformer.toNodeDetailDTO(nodeRepository.getById(id).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.NODE, "id", id.toString()))); } /** @@ -85,7 +84,7 @@ public class NodeService { * @throws NodeNotFoundException if no node is found with the specified ID. */ public Integer deleteNode(Integer id) { - return nodeRepository.setDeprecatedById(id).orElseThrow(() -> new NodeNotFoundException(id)); + return nodeRepository.setDeprecatedById(id).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.NODE, "id", id.toString())); } /** @@ -96,7 +95,7 @@ public class NodeService { * @throws NodeNotFoundException if no node is found for the update. */ public Integer updateNode(NodeUpdateDTO dto) { - return nodeRepository.update(nodeUpdateDTOTransformer.fromNodeUpdateDTO(dto)).orElseThrow(() -> new NodeNotFoundException(dto.getId())); + return nodeRepository.update(nodeUpdateDTOTransformer.fromNodeUpdateDTO(dto)).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.NODE, "id", dto.getId().toString())); } /** diff --git a/src/main/java/de/avatic/lcc/service/access/PremisesService.java b/src/main/java/de/avatic/lcc/service/access/PremisesService.java index 2f425e2..40eaa70 100644 --- a/src/main/java/de/avatic/lcc/service/access/PremisesService.java +++ b/src/main/java/de/avatic/lcc/service/access/PremisesService.java @@ -1,6 +1,5 @@ package de.avatic.lcc.service.access; -import de.avatic.lcc.dto.bulk.BulkStatus; import de.avatic.lcc.dto.calculation.CalculationStatus; import de.avatic.lcc.dto.calculation.PremiseDTO; import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; @@ -19,6 +18,7 @@ import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; import de.avatic.lcc.service.calculation.execution.CalculationStatusService; import de.avatic.lcc.service.transformer.generic.DimensionTransformer; import de.avatic.lcc.service.transformer.premise.PremiseTransformer; +import de.avatic.lcc.util.exception.base.InternalErrorException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -58,6 +58,9 @@ public class PremisesService { //TODO check if user is admin. if not: use the user_id of currently authorized user. var admin = false; + //TODO use actual user. + userId = 1; + return SearchQueryResult.map(premiseRepository.listPremises(filter, new SearchQueryPagination(page, limit), userId, deleted, archived, done), admin ? premiseTransformer::toPremiseDTOWithUserInfo : premiseTransformer::toPremiseDTO); } @@ -76,7 +79,7 @@ public class PremisesService { var userId = 1; // TODO get current user id var validSetId = propertySetRepository.getValidSetId(); - var validPeriodId = validityPeriodRepository.getValidPeriodId(); + var validPeriodId = validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new InternalErrorException("no valid period found that is VALID")); var calculationIds = new ArrayList<>(); diff --git a/src/main/java/de/avatic/lcc/service/access/UserNodeService.java b/src/main/java/de/avatic/lcc/service/access/UserNodeService.java index 8b89a9e..7e0d7cc 100644 --- a/src/main/java/de/avatic/lcc/service/access/UserNodeService.java +++ b/src/main/java/de/avatic/lcc/service/access/UserNodeService.java @@ -5,6 +5,8 @@ import de.avatic.lcc.model.country.IsoCode; import de.avatic.lcc.model.nodes.Node; import de.avatic.lcc.repositories.country.CountryRepository; import de.avatic.lcc.repositories.users.UserNodeRepository; +import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException; +import de.avatic.lcc.util.exception.badrequest.NotFoundException; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -31,8 +33,19 @@ public class UserNodeService { * @throws IllegalArgumentException if any required information in the DTO is missing or invalid. */ public void addUserNode(AddUserNodeDTO dto) { + + // TODO: get real user id. + var userId = 1; + Node node = new Node(); + if (dto.getName() == null || dto.getName().isBlank()) + throw new InvalidArgumentException("name set to blank value"); + if (dto.getAddress() == null || dto.getAddress().isBlank()) + throw new IllegalArgumentException("address set to blank value"); + if (dto.getLocation() == null) + throw new InvalidArgumentException("location set to null value"); + node.setName(dto.getName()); node.setAddress(dto.getAddress()); node.setGeoLng(BigDecimal.valueOf(dto.getLocation().getLongitude())); @@ -41,8 +54,9 @@ public class UserNodeService { node.setSource(true); node.setIntermediate(false); node.setDeprecated(false); - node.setCountryId(countryRepository.getByIsoCode(IsoCode.valueOf(dto.getCountry().getIsoCode())).orElseThrow().getId()); - userNodeRepository.add(node); + node.setCountryId(countryRepository.getByIsoCode(IsoCode.valueOf(dto.getCountry().getIsoCode())).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.COUNTRY, "iso code", dto.getCountry().getIsoCode())).getId()); + + userNodeRepository.add(userId, node); } } diff --git a/src/main/java/de/avatic/lcc/service/access/ValidityPeriodService.java b/src/main/java/de/avatic/lcc/service/access/ValidityPeriodService.java index bb2ea4c..20c9a32 100644 --- a/src/main/java/de/avatic/lcc/service/access/ValidityPeriodService.java +++ b/src/main/java/de/avatic/lcc/service/access/ValidityPeriodService.java @@ -3,6 +3,7 @@ package de.avatic.lcc.service.access; import de.avatic.lcc.dto.generic.ValidityPeriodDTO; import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; import de.avatic.lcc.service.transformer.rates.ValidityPeriodTransformer; +import de.avatic.lcc.util.exception.badrequest.NotFoundException; import org.springframework.stereotype.Service; import java.util.List; @@ -56,6 +57,8 @@ public class ValidityPeriodService { * @param id The unique identifier of the validity period to invalidate */ public void invalidate(Integer id) { - validityPeriodRepository.invalidateById(id); + if (!validityPeriodRepository.invalidateById(id)) + throw new NotFoundException(NotFoundException.NotFoundType.EXPIRED_VALIDITY_PERIOD, "id", id.toString()); + } } diff --git a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java index a787814..83e2ed4 100644 --- a/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java +++ b/src/main/java/de/avatic/lcc/service/bulk/BulkExportService.java @@ -6,6 +6,7 @@ import de.avatic.lcc.model.bulk.HiddenTableType; import de.avatic.lcc.repositories.rates.ValidityPeriodRepository; import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider; import de.avatic.lcc.service.excelMapper.*; +import de.avatic.lcc.util.exception.base.InternalErrorException; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.SheetVisibility; @@ -48,7 +49,7 @@ public class BulkExportService { } public InputStreamSource generateExport(BulkFileType bulkFileType) throws IOException { - return generateExport(bulkFileType, validityPeriodRepository.getValidPeriodId()); + return generateExport(bulkFileType, validityPeriodRepository.getValidPeriodId().orElseThrow(() -> new InternalErrorException("No valid period found that is VALID"))); } public InputStreamSource generateExport(BulkFileType bulkFileType, Integer periodId) throws IOException { diff --git a/src/main/java/de/avatic/lcc/service/calculation/ChangeSupplierService.java b/src/main/java/de/avatic/lcc/service/calculation/ChangeSupplierService.java index 62ca4ec..6ad1c3f 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/ChangeSupplierService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/ChangeSupplierService.java @@ -6,7 +6,6 @@ import de.avatic.lcc.model.nodes.Node; import de.avatic.lcc.model.packaging.PackagingDimension; import de.avatic.lcc.model.premises.Premise; import de.avatic.lcc.model.premises.route.Destination; -import de.avatic.lcc.model.premises.route.RouteSectionInformation; import de.avatic.lcc.model.properties.PackagingProperty; import de.avatic.lcc.model.properties.PackagingPropertyMappingId; import de.avatic.lcc.repositories.NodeRepository; diff --git a/src/main/java/de/avatic/lcc/service/calculation/PremiseCreationService.java b/src/main/java/de/avatic/lcc/service/calculation/PremiseCreationService.java index 11add89..2f49fd1 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/PremiseCreationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/PremiseCreationService.java @@ -1,18 +1,30 @@ package de.avatic.lcc.service.calculation; import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; +import de.avatic.lcc.model.packaging.PackagingDimension; import de.avatic.lcc.model.premises.Premise; import de.avatic.lcc.model.premises.PremiseState; -import de.avatic.lcc.repositories.premise.DestinationRepository; +import de.avatic.lcc.model.properties.PackagingProperty; +import de.avatic.lcc.model.properties.PackagingPropertyMappingId; +import de.avatic.lcc.repositories.MaterialRepository; +import de.avatic.lcc.repositories.NodeRepository; +import de.avatic.lcc.repositories.packaging.PackagingDimensionRepository; +import de.avatic.lcc.repositories.packaging.PackagingPropertiesRepository; +import de.avatic.lcc.repositories.packaging.PackagingRepository; import de.avatic.lcc.repositories.premise.PremiseRepository; -import de.avatic.lcc.repositories.premise.RouteRepository; +import de.avatic.lcc.repositories.users.UserNodeRepository; +import de.avatic.lcc.service.CustomApiService; import de.avatic.lcc.service.access.DestinationService; +import de.avatic.lcc.service.transformer.generic.DimensionTransformer; import de.avatic.lcc.service.transformer.premise.PremiseTransformer; +import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException; +import de.avatic.lcc.util.exception.badrequest.NotFoundException; +import de.avatic.lcc.util.exception.base.ForbiddenException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.math.BigDecimal; +import java.util.*; import java.util.stream.Stream; @Service @@ -20,65 +32,188 @@ public class PremiseCreationService { private final PremiseRepository premiseRepository; private final PremiseTransformer premiseTransformer; private final DestinationService destinationService; + private final UserNodeRepository userNodeRepository; + private final NodeRepository nodeRepository; + private final MaterialRepository materialRepository; + private final DimensionTransformer dimensionTransformer; + private final PackagingRepository packagingRepository; + private final PackagingDimensionRepository packagingDimensionRepository; + private final PackagingPropertiesRepository packagingPropertiesRepository; + private final CustomApiService customApiService; - public PremiseCreationService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DestinationService destinationService) { + public PremiseCreationService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DestinationService destinationService, UserNodeRepository userNodeRepository, NodeRepository nodeRepository, MaterialRepository materialRepository, DimensionTransformer dimensionTransformer, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, CustomApiService customApiService) { this.premiseRepository = premiseRepository; this.premiseTransformer = premiseTransformer; this.destinationService = destinationService; + this.userNodeRepository = userNodeRepository; + this.nodeRepository = nodeRepository; + this.materialRepository = materialRepository; + this.dimensionTransformer = dimensionTransformer; + this.packagingRepository = packagingRepository; + this.packagingDimensionRepository = packagingDimensionRepository; + this.packagingPropertiesRepository = packagingPropertiesRepository; + this.customApiService = customApiService; } + @Transactional public List createPremises(List materialIds, List supplierIds, List userSupplierIds, boolean createEmpty) { - Integer userId = 1; //TODO get user id - List foundPremises = new ArrayList<>(premiseRepository.getPremisesByMaterialIdsAndSupplierIds(materialIds, supplierIds, userSupplierIds, userId, createEmpty).stream().map(TemporaryPremise::new).toList()); + /* Build all resulting premises */ + List premises = Stream.concat( + supplierIds.stream().flatMap(id -> materialIds.stream().map(materialId -> new TemporaryPremise(materialId, id, null, false))), + userSupplierIds.stream().flatMap(id -> materialIds.stream().map(materialId -> new TemporaryPremise(materialId, null, id, true)))).toList(); - List toBeCopied = foundPremises.stream().filter(p -> (!p.getPremise().getState().equals(PremiseState.DRAFT) || !Objects.equals(p.getPremise().getUserId(), userId))).toList(); + premises.forEach(p -> findExistingPremise(p, createEmpty, userId)); - List toBeCreated = materialIds.stream() - .flatMap(materialId -> { - Stream supplierCombinations = supplierIds.stream() - .map(supplierId -> new TemporaryPremise(materialId, supplierId, null, false)); + premises.forEach(p -> verifyNode(p, userId)); + verifyMaterial(materialIds); - Stream userSupplierCombinations = - (userSupplierIds != null && !userSupplierIds.isEmpty()) ? - userSupplierIds.stream() - .map(userSupplierId -> new TemporaryPremise(materialId, null, userSupplierId, true)) : - Stream.empty(); + premises.forEach(p -> { + if (p.getPremise() == null) { // create new - return Stream.concat(supplierCombinations, userSupplierCombinations); - }) - // filter existing combinations. - .filter(p -> foundPremises.stream() - .noneMatch(found -> - Objects.equals(found.materialId, p.materialId) && - Objects.equals(found.supplierId, p.supplierId) && - Objects.equals(found.userSupplierId, p.userSupplierId) - ) - ) - .toList(); + p.setId(premiseRepository.insert(p.getMaterialId(), p.getSupplierId(), p.getUserSupplierId(), p.getGeoLat(), p.getGeoLng(), p.getCountryId(), userId)); + fillPremise(p, userId); - List premiseIds = new ArrayList<>(toBeCreated.stream().map(p -> premiseRepository.insert(p.getMaterialId(), p.getSupplierId(), p.getUserSupplierId(), userId)).toList()); + } else if (p.getPremise().getState().equals(PremiseState.DRAFT)) { // recycle + p.setId(p.getPremise().getId()); + if (createEmpty) { + // reset to defaults. + fillPremise(p, userId); + } - for( TemporaryPremise p : toBeCopied ) { - Integer id = premiseRepository.insert(p.getMaterialId(), p.getSupplierId(), p.getUserSupplierId(), userId); - destinationService.duplicate(p.getPremise().getId(), id); - premiseIds.add(id); - } + } else if (p.getPremise().getState().equals(PremiseState.COMPLETED)) { + // create a copy + p.setId(premiseRepository.insert(p.getMaterialId(), p.getSupplierId(), p.getUserSupplierId(), p.getGeoLat(), p.getGeoLng(), p.getCountryId(), userId)); + copyPremise(p, userId); + destinationService.duplicate(p.getPremise().getId(), p.getId()); + } + }); - return premiseRepository.getPremisesById(premiseIds).stream().map(premiseTransformer::toPremiseDetailDTO).toList(); + return premiseRepository.getPremisesById(premises.stream().map(TemporaryPremise::getId).toList()).stream().map(premiseTransformer::toPremiseDetailDTO).toList(); } + private void copyPremise(TemporaryPremise p, Integer userId) { + var old = p.getPremise(); + premiseRepository.updateMaterial(Collections.singletonList(p.getId()), userId, old.getHsCode(), old.getCustomRate()); + premiseRepository.updatePrice(Collections.singletonList(p.getId()), userId, old.getMaterialCost(), old.getFcaEnabled(), old.getOverseaShare()); + premiseRepository.updatePackaging(Collections.singletonList(p.getId()), userId, dimensionTransformer.toDimensionEntity(old), old.getHuStackable(), old.getHuMixable()); + premiseRepository.setPackagingId(p.getId(), old.getId()); + } + + private void fillPremise(TemporaryPremise p, Integer userId) { + + if (!p.isUserSupplier()) { + var packaging = packagingRepository.getByMaterialIdAndSupplierId(p.getMaterialId(), p.getSupplierId()); + Optional hu = packagingDimensionRepository.getById(packaging.getFirst().getHuId()); + Optional shu = packagingDimensionRepository.getById(packaging.getFirst().getShuId()); + + if (hu.isPresent() && shu.isPresent()) { + boolean stackable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.getFirst().getId(), PackagingPropertyMappingId.STACKABLE.name()).map(PackagingProperty::getValue).map(Boolean::valueOf).orElse(false); + boolean mixable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.getFirst().getId(), PackagingPropertyMappingId.MIXABLE.name()).map(PackagingProperty::getValue).map(Boolean::valueOf).orElse(false); + premiseRepository.updatePackaging(Collections.singletonList(p.getId()), userId, hu.get(), shu.get(), stackable, mixable); //TODO clarify if the hu unit count in packaging data is total unit count or shu count (shu*hu or hu) + premiseRepository.setPackagingId(p.getId(), packaging.getFirst().getId()); + } + } + + var material = materialRepository.getById(p.getMaterialId()); + material.ifPresent(value -> premiseRepository.updateMaterial(Collections.singletonList(p.getId()), userId, value.getHsCode(), customApiService.getTariffRate(value.getHsCode(), getCountryId(p)))); + + } + + private Integer getCountryId(TemporaryPremise p) { + + if (p.isUserSupplier()) { + return userNodeRepository.getById(p.getUserSupplierId()).orElseThrow().getCountryId(); + } else { + return nodeRepository.getById(p.getSupplierId()).orElseThrow().getCountryId(); + } + } + + private void findExistingPremise(TemporaryPremise premise, boolean createEmpty, Integer userId) { + var existingPremises = premiseRepository.findByMaterialIdAndSupplierId(premise.getMaterialId(), premise.getSupplierId(), premise.getUserSupplierId(), userId); + Premise existingDraft = existingPremises.stream().filter(p -> p.getState().equals(PremiseState.DRAFT)).findFirst().orElse(null); + + if (existingDraft != null) { + premise.setPremise(existingDraft); + } else if (!createEmpty) { + Premise youngestCompleted = existingPremises.stream().filter(p -> p.getState().equals(PremiseState.COMPLETED) && p.getUserId().equals(userId)).findFirst().orElse(null); + + // no completed from current user, check if completed from other user. + if (youngestCompleted == null) { + youngestCompleted = existingPremises.stream().filter(p -> p.getState().equals(PremiseState.COMPLETED)).findFirst().orElse(null); + } + + premise.setPremise(youngestCompleted); + } + } + + /** + * Checks if the node of the given temporary premiss exists, and updates data in + * temporary premiss. + * + * @param temporaryPremise + */ + private void verifyNode(TemporaryPremise temporaryPremise, Integer userId) { + + var node = temporaryPremise.isUserSupplier() ? + userNodeRepository.getById(temporaryPremise.getUserSupplierId()).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.USER_NODE, "id", String.valueOf(temporaryPremise.getUserSupplierId()))) : + nodeRepository.getById(temporaryPremise.getSupplierId()).orElseThrow(() -> new NotFoundException(NotFoundException.NotFoundType.NODE, "id", String.valueOf(temporaryPremise.getSupplierId()))); + + if(temporaryPremise.isUserSupplier()) { + var id = userNodeRepository.getOwnerById(temporaryPremise.getUserSupplierId()); + + if(id.isPresent() && !id.get().equals(userId)) { + throw new ForbiddenException("Unable to access this node id " + temporaryPremise.getUserSupplierId()); + } + } + + if (!node.getSource()) + throw new InvalidArgumentException((temporaryPremise.isUserSupplier() ? "User node" : "Node") + " with id " + node.getId() + " is not a source node."); + + temporaryPremise.setGeoLat(node.getGeoLat()); + temporaryPremise.setGeoLng(node.getGeoLng()); + temporaryPremise.setCountryId(node.getCountryId()); + + } + + private void verifyMaterial(List materialIds) { + var missingMaterials = materialRepository.findMissingIds(materialIds); + + if (!missingMaterials.isEmpty()) + throw new InvalidArgumentException("Material with ids " + missingMaterials + " does not exist."); + } private static class TemporaryPremise { + + private Integer materialId; private Integer supplierId; private Integer userSupplierId; private boolean isUserSupplier; + /* found premise */ private Premise premise; + private BigDecimal geoLat; + private BigDecimal geoLng; + + private Integer countryId; + + /* id of the premise used (can be newly created or recycled one)*/ + private Integer id; + + + public TemporaryPremise(Integer materialId, Integer supplierId, Integer userSupplierId, boolean isUserSupplier) { + this.materialId = materialId; + this.supplierId = supplierId; + this.userSupplierId = userSupplierId; + this.isUserSupplier = isUserSupplier; + this.premise = null; + + } + public Integer getMaterialId() { return materialId; } @@ -111,29 +246,48 @@ public class PremiseCreationService { isUserSupplier = userSupplier; } + public Premise getPremise() { + return premise; + } + public void setPremise(Premise premise) { this.premise = premise; } - public TemporaryPremise(Premise premise) { - this.premise = premise; - this.materialId = premise.getMaterialId(); - this.supplierId = premise.getSupplierNodeId(); - this.userSupplierId = premise.getUserSupplierNodeId(); - this.isUserSupplier = premise.getUserSupplierNodeId() != null; + public BigDecimal getGeoLat() { + return this.geoLat; + } + + public void setGeoLat(BigDecimal geoLat) { + this.geoLat = geoLat; + } + + public BigDecimal getGeoLng() { + return this.geoLng; + } + + public void setGeoLng(BigDecimal geoLng) { + this.geoLng = geoLng; + } + + public Integer getCountryId() { + return this.countryId; + } + + public void setCountryId(Integer countryId) { + this.countryId = countryId; + } + + public void addFoundPremises(List foundPremises) { } - public TemporaryPremise(Integer materialId, Integer supplierId, Integer userSupplierId, boolean isUserSupplier) { - this.materialId = materialId; - this.supplierId = supplierId; - this.userSupplierId = userSupplierId; - this.isUserSupplier = isUserSupplier; - this.premise = null; + public Integer getId() { + return this.id; } - public Premise getPremise() { - return premise; + public void setId(Integer id) { + this.id = id; } } } diff --git a/src/main/java/de/avatic/lcc/service/calculation/PremiseSearchStringAnalyzerService.java b/src/main/java/de/avatic/lcc/service/calculation/PremiseSearchStringAnalyzerService.java index ad80421..8ece6d1 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/PremiseSearchStringAnalyzerService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/PremiseSearchStringAnalyzerService.java @@ -5,6 +5,7 @@ import de.avatic.lcc.model.materials.Material; import de.avatic.lcc.repositories.MaterialRepository; import de.avatic.lcc.repositories.NodeRepository; import de.avatic.lcc.repositories.premise.PremiseRepository; +import de.avatic.lcc.repositories.users.UserNodeRepository; import de.avatic.lcc.service.transformer.generic.MaterialTransformer; import de.avatic.lcc.service.transformer.generic.NodeTransformer; import org.springframework.stereotype.Service; @@ -31,6 +32,7 @@ public class PremiseSearchStringAnalyzerService { private final PremiseRepository premiseRepository; private final NodeTransformer nodeTransformer; private final MaterialTransformer materialTransformer; + private final UserNodeRepository userNodeRepository; /** * Constructor for the PremiseSearchStringAnalyzerService, initializing the required repositories @@ -42,12 +44,13 @@ public class PremiseSearchStringAnalyzerService { * @param nodeTransformer Transformer for mapping database node entities (suppliers) to DTOs. * @param materialTransformer Transformer for mapping database material entities to DTOs. */ - public PremiseSearchStringAnalyzerService(MaterialRepository materialRepository, NodeRepository nodeRepository, PremiseRepository premiseRepository, NodeTransformer nodeTransformer, MaterialTransformer materialTransformer) { + public PremiseSearchStringAnalyzerService(MaterialRepository materialRepository, NodeRepository nodeRepository, PremiseRepository premiseRepository, NodeTransformer nodeTransformer, MaterialTransformer materialTransformer, UserNodeRepository userNodeRepository) { this.materialRepository = materialRepository; this.nodeRepository = nodeRepository; this.premiseRepository = premiseRepository; this.nodeTransformer = nodeTransformer; this.materialTransformer = materialTransformer; + this.userNodeRepository = userNodeRepository; } /** @@ -63,18 +66,21 @@ public class PremiseSearchStringAnalyzerService { * and user-specific suppliers related to the identified materials. */ public PremiseSearchResultDTO findMaterialAndSuppliers(String search) { + + var userId = 1; //TODO get actual user id. + List material = materialRepository.getByPartNumbers(findPartNumbers(search)); List materialIds = material.stream().map(Material::getId).toList(); // find suppliers associated with this material. List supplierIds = premiseRepository.findAssociatedSuppliers(materialIds); - List userSupplierIds = premiseRepository.findAssociatedUserSuppliers(materialIds); + List userSupplierIds = premiseRepository.findAssociatedUserSuppliers(materialIds, userId); var dto = new PremiseSearchResultDTO(); dto.setMaterials(material.stream().map(materialTransformer::toMaterialDTO).toList()); dto.setSupplier(nodeRepository.getByIds(supplierIds).stream().map(nodeTransformer::toNodeDTO).toList()); - dto.setUserSupplier(nodeRepository.getByIds(userSupplierIds).stream().map(nodeTransformer::toNodeDTO).toList()); + dto.setUserSupplier(userNodeRepository.getByIds(userSupplierIds).stream().map(nodeTransformer::toNodeDTO).toList()); return dto; } diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java index c548260..e26ec94 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/CustomCostCalculationService.java @@ -76,19 +76,19 @@ public class CustomCostCalculationService { } var customValue = materialCost.add(fcaFee).add(transportationCost); - var customDuties = customValue.multiply(BigDecimal.valueOf(tariffRate)); + var customDuties = customValue.multiply(tariffRate); var annualCustomFee = shippingFrequency * customFee; var annualCost = customDuties.add(BigDecimal.valueOf(annualCustomFee)); var customRiskValue = materialCost.add(fcaFee).add(transportationRiskCost); - var customRiskDuties = customRiskValue.multiply(BigDecimal.valueOf(tariffRate)); + var customRiskDuties = customRiskValue.multiply(tariffRate); var annualRiskCost = customRiskDuties.add(BigDecimal.valueOf(annualCustomFee)); var customChanceValue = materialCost.add(fcaFee).add(transportationChanceCost); - var customChanceDuties = customChanceValue.multiply(BigDecimal.valueOf(tariffRate)); + var customChanceDuties = customChanceValue.multiply(tariffRate); var annualChanceCost = customChanceDuties.add(BigDecimal.valueOf(annualCustomFee)); - return new CustomResult(customValue, customRiskValue, customChanceValue, customDuties, BigDecimal.valueOf(tariffRate), annualCost, annualRiskCost, annualChanceCost); + return new CustomResult(customValue, customRiskValue, customChanceValue, customDuties, tariffRate, annualCost, annualRiskCost, annualChanceCost); } private List getCustomRelevantRouteSections(List sections) { diff --git a/src/main/java/de/avatic/lcc/service/transformer/generic/DimensionTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/generic/DimensionTransformer.java index 22fb0b4..a9e677e 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/generic/DimensionTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/generic/DimensionTransformer.java @@ -42,10 +42,27 @@ public class DimensionTransformer { return entity; } + public PackagingDimension toDimensionEntity(Premise entity) { + var packaging = new PackagingDimension(); + + packaging.setId(null); + packaging.setType(PackagingType.HU); + packaging.setLength(entity.getIndividualHuLength()); + packaging.setWidth(entity.getIndividualHuWidth()); + packaging.setHeight(entity.getIndividualHuHeight()); + packaging.setDimensionUnit(entity.getHuDisplayedDimensionUnit()); + packaging.setWeight(entity.getIndividualHuWeight()); + packaging.setWeightUnit(entity.getHuDisplayedWeightUnit()); + packaging.setContentUnitCount(entity.getHuUnitCount()); + packaging.setDeprecated(null); + + return packaging; + } + public DimensionDTO toDimensionDTO(Premise entity) { DimensionDTO dto = new DimensionDTO(); - dto.setId(entity.getId()); + dto.setId(null); dto.setType(PackagingType.HU); dto.setLength(entity.getHuDisplayedDimensionUnit().convertFromMM(entity.getIndividualHuLength()).doubleValue()); dto.setWidth(entity.getHuDisplayedDimensionUnit().convertFromMM(entity.getIndividualHuWidth()).doubleValue()); diff --git a/src/main/java/de/avatic/lcc/service/transformer/generic/NodeTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/generic/NodeTransformer.java index da6d8d8..1913f8e 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/generic/NodeTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/generic/NodeTransformer.java @@ -38,6 +38,7 @@ public class NodeTransformer { dto.setDeprecated(entity.getDeprecated()); dto.setLocation(locationTransformer.toLocationDTO(entity)); dto.setUserNode(false); + dto.setExternalMappingId(entity.getExternalMappingId()); return dto; } diff --git a/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeDetailTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeDetailTransformer.java index 685d458..e44eb6d 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeDetailTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeDetailTransformer.java @@ -37,7 +37,9 @@ public class NodeDetailTransformer { var chains = new ArrayList>(); - for (var chain : node.getNodePredecessors()) { + var foundChains = node.getNodePredecessors(); + + for (var chain : foundChains) { Map predecessorChain = new HashMap<>(); for (Integer seq : chain.keySet()) predecessorChain.put(seq, nodeTransformer.toNodeDTO(nodeRepository.getById(chain.get(seq)).orElseThrow())); @@ -53,6 +55,7 @@ public class NodeDetailTransformer { dto.setTypes(toNodeTypeArrayList(node)); dto.setPredecessors(chains); dto.setOutboundCountries(node.getOutboundCountries().stream().map(id -> countryTransformer.toCountryDTO(countryRepository.getById(id).orElseThrow())).toList()); + dto.setExternalMappingId(node.getExternalMappingId()); return dto; } diff --git a/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeUpdateDTOTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeUpdateDTOTransformer.java index b5cfbac..ec38806 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeUpdateDTOTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/nodes/NodeUpdateDTOTransformer.java @@ -1,21 +1,24 @@ package de.avatic.lcc.service.transformer.nodes; import de.avatic.lcc.dto.configuration.nodes.update.NodeUpdateDTO; +import de.avatic.lcc.dto.generic.NodeType; import de.avatic.lcc.model.nodes.Node; +import de.avatic.lcc.repositories.country.CountryRepository; +import org.apache.commons.lang3.NotImplementedException; import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.util.ArrayList; + @Service public class NodeUpdateDTOTransformer { + + public Node fromNodeUpdateDTO(NodeUpdateDTO dto) { + throw new NotImplementedException("Not yet implemented fromNodeUpdateDTO"); - Node entity = new Node(); - - entity.setId(dto.getId()); - //entity.setSink(dto.get); - - return entity; } } diff --git a/src/main/java/de/avatic/lcc/service/transformer/premise/PremiseTransformer.java b/src/main/java/de/avatic/lcc/service/transformer/premise/PremiseTransformer.java index 6009354..8bb56fa 100644 --- a/src/main/java/de/avatic/lcc/service/transformer/premise/PremiseTransformer.java +++ b/src/main/java/de/avatic/lcc/service/transformer/premise/PremiseTransformer.java @@ -8,10 +8,10 @@ import de.avatic.lcc.dto.generic.NodeDTO; import de.avatic.lcc.dto.generic.NodeType; import de.avatic.lcc.model.premises.Premise; import de.avatic.lcc.model.premises.PremiseListEntry; -import de.avatic.lcc.repositories.premise.DestinationRepository; import de.avatic.lcc.repositories.MaterialRepository; import de.avatic.lcc.repositories.NodeRepository; import de.avatic.lcc.repositories.country.CountryRepository; +import de.avatic.lcc.repositories.premise.DestinationRepository; import de.avatic.lcc.repositories.users.UserNodeRepository; import de.avatic.lcc.repositories.users.UserRepository; import de.avatic.lcc.service.transformer.generic.CountryTransformer; @@ -73,6 +73,7 @@ public class PremiseTransformer { node.setAddress(entity.getSupplierAddress()); node.setLocation(new LocationDTO(entity.getSupplierGeoLatitude().doubleValue(), entity.getSupplierGeoLongitude().doubleValue())); node.setTypes(types); + node.setDeprecated(entity.getDeprecated()); PremiseDTO dto = new PremiseDTO(); @@ -94,12 +95,15 @@ public class PremiseTransformer { dto.setMixable(entity.getHuMixable()); dto.setStackable(entity.getHuStackable()); - dto.setDimension(dimensionTransformer.toDimensionDTO(entity)); + if (entity.getIndividualHuHeight() == null || entity.getIndividualHuWidth() == null || entity.getIndividualHuLength() == null || entity.getIndividualHuWeight() == null) + dto.setDimension(null); + else + dto.setDimension(dimensionTransformer.toDimensionDTO(entity)); if (entity.getSupplierNodeId() != null) dto.setSupplier(nodeRepository.getById(entity.getSupplierNodeId()).map(nodeTransformer::toNodeDTO).orElseThrow()); - if(entity.getUserSupplierNodeId() != null) { + if (entity.getUserSupplierNodeId() != null) { dto.setSupplier(userNodeRepository.getById(entity.getUserSupplierNodeId()).map(nodeTransformer::toNodeDTO).orElseThrow()); dto.getSupplier().setUserNode(true); } diff --git a/src/main/java/de/avatic/lcc/util/exception/badrequest/InvalidArgumentException.java b/src/main/java/de/avatic/lcc/util/exception/badrequest/InvalidArgumentException.java new file mode 100644 index 0000000..931d5f2 --- /dev/null +++ b/src/main/java/de/avatic/lcc/util/exception/badrequest/InvalidArgumentException.java @@ -0,0 +1,15 @@ +package de.avatic.lcc.util.exception.badrequest; + +import de.avatic.lcc.util.exception.base.BadRequestException; + +public class InvalidArgumentException extends BadRequestException { + + public InvalidArgumentException(String parameterName, String parameterValue) { + super("Invalid Parameter", "Parameter " + parameterName + " set to invalid value " + parameterValue, null); + } + + public InvalidArgumentException(String errorText) { + super("Invalid Parameter", errorText, null); + } + +} diff --git a/src/main/java/de/avatic/lcc/util/exception/badrequest/NotFoundException.java b/src/main/java/de/avatic/lcc/util/exception/badrequest/NotFoundException.java index 92239d1..3d0bc53 100644 --- a/src/main/java/de/avatic/lcc/util/exception/badrequest/NotFoundException.java +++ b/src/main/java/de/avatic/lcc/util/exception/badrequest/NotFoundException.java @@ -4,20 +4,6 @@ import de.avatic.lcc.util.exception.base.BadRequestException; public class NotFoundException extends BadRequestException { - public enum NotFoundType { - PROPERTY("Property"), PROPERTY_SET("Property set"), EXPIRED_PROPERTY_SET("Expired property set"), MATERIAL("Material"), NODE("Node"), COUNTRY("Country"), COUNTRY_PROPERTY("Country property"); - - private final String identifier; - - NotFoundType(String identifier) { - this.identifier = identifier; - } - - public String getIdentifier() { - return identifier; - } - } - public NotFoundException(NotFoundType type, String identifier, String value, Exception e) { super(type.getIdentifier() + " not found", type.getIdentifier() + " with " + identifier + " " + value + " not found", e); } @@ -30,8 +16,30 @@ public class NotFoundException extends BadRequestException { super(type.getIdentifier() + " not found", type.getIdentifier() + " with " + value + " not found"); } - public NotFoundException(NotFoundType type, String value, Exception e) { + public NotFoundException(NotFoundType type, String value, Exception e) { super(type.getIdentifier() + " not found", type.getIdentifier() + " with " + value + " not found", e); } + public enum NotFoundType { + PROPERTY("Property"), + PROPERTY_SET("Property set"), + EXPIRED_PROPERTY_SET("Expired property set"), + MATERIAL("Material"), NODE("Node"), + COUNTRY("Country"), + COUNTRY_PROPERTY("Country property"), + CONTAINER_RATE("Container rate"), + EXPIRED_VALIDITY_PERIOD("Expired validity period"), + USER_NODE("User node"); + + private final String identifier; + + NotFoundType(String identifier) { + this.identifier = identifier; + } + + public String getIdentifier() { + return identifier; + } + } + } diff --git a/src/main/java/de/avatic/lcc/util/exception/base/ForbiddenException.java b/src/main/java/de/avatic/lcc/util/exception/base/ForbiddenException.java new file mode 100644 index 0000000..3224b41 --- /dev/null +++ b/src/main/java/de/avatic/lcc/util/exception/base/ForbiddenException.java @@ -0,0 +1,42 @@ +package de.avatic.lcc.util.exception.base; + +public class ForbiddenException extends RuntimeException{ + + String title; + String message; + + public ForbiddenException(String message) { + super(message); + this.title = "Forbidden Error"; + this.message = message; + } + + public ForbiddenException(String message, Exception trace) { + super(message, trace); + this.title = "Forbidden Error"; + this.message = message; + } + + public ForbiddenException(String title, String message, Exception trace) { + super(message, trace); + this.title = title; + this.message = message; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/resources/master_data/03-nodes.sql b/src/main/resources/master_data/03-nodes.sql index 1eff418..a720a30 100644 --- a/src/main/resources/master_data/03-nodes.sql +++ b/src/main/resources/master_data/03-nodes.sql @@ -1,8 +1,7 @@ --- SQL INSERT-Statements für die node-Tabelle --- Generiert aus Lastenheft_Requirements Appendix B_Transportknoten.csv --- 49 Einträge mit korrekten external mapping IDs aus der ersten CSV-Spalte +-- Generated SQL INSERT statements for nodes +-- Generated from nodes.xlsx using countries.xlsx for ISO code mapping --- Node 1: Xiamen +-- Node 1: Xiamen (Metropolitan area) INSERT INTO node ( id, country_id, @@ -25,11 +24,11 @@ INSERT INTO node ( false, false, true, - 24.4890, + 24.489, 118.1478 ); --- Node 2: Shanghai +-- Node 2: Shanghai (Metropolitan area) INSERT INTO node ( id, country_id, @@ -56,7 +55,7 @@ INSERT INTO node ( 121.4858 ); --- Node 3: Hangzhou +-- Node 3: Hangzhou (Metropolitan area) INSERT INTO node ( id, country_id, @@ -83,7 +82,7 @@ INSERT INTO node ( 120.1701 ); --- Node 4: Yangzhong +-- Node 4: Yangzhong (Metropolitan area) INSERT INTO node ( id, country_id, @@ -100,17 +99,17 @@ INSERT INTO node ( 4, (SELECT id FROM country WHERE iso_code = 'CN'), 'Yangzhong (Metropolitan area)', - 'Yangzhong, Zhenjiang, Jiangsu, China', + 'CN 江苏省 镇江市 扬中市 11-107, China, 212299', 'Yangzhong', false, false, false, true, - 32.2370, - 119.7810 + 32.251, + 119.8078 ); --- Node 5: Taicang +-- Node 5: Taicang (Metropolitan area) INSERT INTO node ( id, country_id, @@ -127,17 +126,17 @@ INSERT INTO node ( 5, (SELECT id FROM country WHERE iso_code = 'CN'), 'Taicang (Metropolitan area)', - 'Taicang, Suzhou, Jiangsu, China', + 'H39C+WP4, Xinbei E Rd, Taicang, Suzhou, Jiangsu, China, 215421', 'Taicang', false, false, false, true, - 31.4500, - 121.1000 + 31.5788, + 121.0729 ); --- Node 6: Jinjiang +-- Node 6: Jinjiang (Metropolitan area) INSERT INTO node ( id, country_id, @@ -154,17 +153,17 @@ INSERT INTO node ( 6, (SELECT id FROM country WHERE iso_code = 'CN'), 'Jinjiang (Metropolitan area)', - 'Jinjiang, Quanzhou, Fujian, China', + '8 Xinzhou Rd, Jingjiang, Taizhou, Jiangsu, China, 214527', 'Jinjiang', false, false, false, true, - 24.8180, - 118.5514 + 31.9836, + 120.2469 ); --- Node 7: Qingdao +-- Node 7: Qingdao (Metropolitan area) INSERT INTO node ( id, country_id, @@ -181,17 +180,17 @@ INSERT INTO node ( 7, (SELECT id FROM country WHERE iso_code = 'CN'), 'Qingdao (Metropolitan area)', - 'Qingdao, Shandong, China', + 'China, 155甲22号CN 山东省 青岛市 李沧区 文昌路 155 邮政编码: 266046', 'Qingdao', false, false, false, true, - 36.0986, - 120.3719 + 36.1867, + 120.4089 ); --- Node 8: Fuqin +-- Node 8: Fuqing (Metropolitan area) INSERT INTO node ( id, country_id, @@ -208,17 +207,17 @@ INSERT INTO node ( 8, (SELECT id FROM country WHERE iso_code = 'CN'), 'Fuqing (Metropolitan area)', - 'Fuqing, Fuzhou, Fujian, China', + 'China, Fujian, Fuzhou, Fuqing, 清昌大道105号万达广场2号门4层 邮政编码: 350300', 'Fuqin', false, false, false, true, - 25.7213, - 119.3842 + 25.7184, + 119.3492 ); --- Node 9: Linfen +-- Node 9: Linfen (Metropolitan area) INSERT INTO node ( id, country_id, @@ -235,17 +234,17 @@ INSERT INTO node ( 9, (SELECT id FROM country WHERE iso_code = 'CN'), 'Linfen (Metropolitan area)', - 'Linfen, Shanxi, China', + '201 Jiefang Rd, Tianya District, Lin Fen Shi, Hainan, China, 572003', 'Linfen', false, false, false, true, - 36.0880, - 111.5189 + 36.0838, + 111.5245 ); --- Node 10: Yantian +-- Node 10: Yantian (Metropolitan area) INSERT INTO node ( id, country_id, @@ -262,17 +261,17 @@ INSERT INTO node ( 10, (SELECT id FROM country WHERE iso_code = 'CN'), 'Yantian (Metropolitan area)', - 'Yantian District, Shenzhen, Guangdong, China', + 'China, CN 广东省 深圳市 盐田区 40 米 邮政编码: 518081', 'Yantian', false, false, false, true, - 22.5881, - 114.2370 + 22.5782, + 114.2623 ); --- Node 11: Bangalore +-- Node 11: Bangalore (Metropolitan area) INSERT INTO node ( id, country_id, @@ -289,17 +288,17 @@ INSERT INTO node ( 11, (SELECT id FROM country WHERE iso_code = 'IN'), 'Bangalore (Metropolitan area)', - 'Bengaluru, Karnataka, India', + '197, Bommasandra Indst. Area Hosur Road, Taluk, Anekal, Bengaluru, Karnataka 560099, Indien', 'Bangalore', false, false, false, true, - 12.9716, - 77.5946 + 12.8205, + 77.6951 ); --- Node 12: Chennai +-- Node 12: Chennai (Metropolitan area) INSERT INTO node ( id, country_id, @@ -316,17 +315,17 @@ INSERT INTO node ( 12, (SELECT id FROM country WHERE iso_code = 'IN'), 'Chennai (Metropolitan area)', - 'Chennai, Tamil Nadu, India', + 'FORT, Tamil Nadu 602106, Indien', 'Chennai', false, false, false, true, - 13.0827, - 80.2707 + 12.9359, + 79.8972 ); --- Node 13: Pune +-- Node 13: Pune (Metropolitan area) INSERT INTO node ( id, country_id, @@ -343,17 +342,17 @@ INSERT INTO node ( 13, (SELECT id FROM country WHERE iso_code = 'IN'), 'Pune (Metropolitan area)', - '201, Solitaire Business Hub, Opp. Viman Nagar, Pune-Nagar Road, Viman Nagar, Pune, Maharashtra 411014, India', + 'Moie, Pimpri-Chinchwad, Maharashtra 411062, Indien', 'Pune', false, false, false, true, - 18.5679, - 73.9143 + 18.6984, + 73.8239 ); --- Node 14: Shenzhen +-- Node 14: Shenzhen (Metropolitan area) INSERT INTO node ( id, country_id, @@ -368,19 +367,19 @@ INSERT INTO node ( geo_lng ) VALUES ( 14, - (SELECT id FROM country WHERE iso_code = 'IN'), + (SELECT id FROM country WHERE iso_code = 'CN'), 'Shenzhen (Metropolitan area)', - 'Shenzhen, Guangdong, China', + 'G3R5+MP9, Fuzhong 3rd Rd, 福田CBD Futian District, Shenzhen, Guangdong Province, China, 518000', 'Shenzhen', false, false, false, true, - 22.5431, - 114.0579 + 22.542, + 114.0593 ); --- Node 15: Aurangabad +-- Node 15: Aurangabad (Metropolitan area) INSERT INTO node ( id, country_id, @@ -397,17 +396,17 @@ INSERT INTO node ( 15, (SELECT id FROM country WHERE iso_code = 'IN'), 'Aurangabad (Metropolitan area)', - 'Aurangabad, Maharashtra, India', + 'Gut no, Teesgaon, 113 B, Ahmednagar - Aurangabad Rd, CIDCO Waluj Mahanagar 2, Waluj, Aurangabad, Dharmapur, Maharashtra 431136, Indien', 'Aurangabad', false, false, false, true, - 19.8762, - 75.3433 + 19.8475, + 75.2671 ); --- Node 16: LX (Linde China) +-- Node 16: Linde (China) Forklift Truck (Supplier) INSERT INTO node ( id, country_id, @@ -424,20 +423,17 @@ INSERT INTO node ( 16, (SELECT id FROM country WHERE iso_code = 'CN'), 'Linde (China) Forklift Truck (Supplier)', - 'Linde (China) Forklift Truck Corp. Ltd. - 1258 Gonghexin Road - 闸北区, 上海市 200070 - People''s Republic of China', + '89 Jinshang Rd, Siming District, Xiamen, Fujian, China, 361009', 'LX', false, false, true, false, - 31.2872, - 121.4581 + 24.489, + 118.1478 ); --- Node 17: JJ (KION Baoli) +-- Node 17: KION Baoli (Jiangsu) Forklift Co., Ltd. INSERT INTO node ( id, country_id, @@ -454,20 +450,17 @@ INSERT INTO node ( 17, (SELECT id FROM country WHERE iso_code = 'CN'), 'KION Baoli (Jiangsu) Forklift Co., Ltd.', - 'KION Baoli (Jiangsu) Forklift Co., Ltd. - No. 88 Gangkou Avenue, Jingjiang Economic Development Zone - Jingjiang City, Jiangsu Province 214500 - People''s Republic of China', + '8 Xinzhou Rd, Jingjiang, Taizhou, Jiangsu, China, 214527', 'JJ', false, false, true, false, - 32.0140, - 120.2692 + 31.9836, + 120.2469 ); --- Node 18: ExM (Expeditors Mumbai) +-- Node 18: Expeditors Mumbai (Warehouse) INSERT INTO node ( id, country_id, @@ -484,17 +477,17 @@ INSERT INTO node ( 18, (SELECT id FROM country WHERE iso_code = 'IN'), 'Expeditors Mumbai (Warehouse)', - 'A-501 Delphi, Hiranandani Business Park, Powai, Mumbai 400076', + 'V2C4+3HC, Khopate, Bandhpada, Maharashtra 410206, Indien', 'ExM', false, false, true, true, - 19.1176, - 72.9060 + 18.8707, + 73.0077 ); --- Node 19: AB (Aschaffenburg) +-- Node 19: Aschaffenburg (KION plant) INSERT INTO node ( id, country_id, @@ -511,17 +504,17 @@ INSERT INTO node ( 19, (SELECT id FROM country WHERE iso_code = 'DE'), 'Aschaffenburg (KION plant)', - 'Thüngenstraße 1, 63743 Aschaffenburg, Germany', + 'Großostheimer Str. 198, 63741 Aschaffenburg', 'AB', - false, + true, true, true, false, - 49.9763, - 9.1432 + 49.9506, + 9.1038 ); --- Node 20: HH (Hamburg) +-- Node 20: Hamburg (KION plant) INSERT INTO node ( id, country_id, @@ -538,17 +531,17 @@ INSERT INTO node ( 20, (SELECT id FROM country WHERE iso_code = 'DE'), 'Hamburg (KION plant)', - 'EUROGATE Container Terminal Hamburg GmbH, Predöhlkai 1, 20457 Hamburg, Germany', + 'Berzeliusstraße 5, 22113 Hamburg', 'HH', true, true, true, false, - 53.5488, - 9.9872 + 53.537, + 10.0844 ); --- Node 21: FGG (Geisa) +-- Node 21: Geisa (KION plant) INSERT INTO node ( id, country_id, @@ -565,17 +558,17 @@ INSERT INTO node ( 21, (SELECT id FROM country WHERE iso_code = 'DE'), 'Geisa (KION plant)', - 'Porsche-Allee 1, 04356 Leipzig, Germany', + 'Schleider Str. 13, 36419 Geisa', 'FGG', - false, + true, true, true, false, - 51.3412, - 12.3747 + 50.7077, + 9.9478 ); --- Node 22: KWS (Reutlingen) +-- Node 22: Reutlingen (KION plant) INSERT INTO node ( id, country_id, @@ -592,17 +585,17 @@ INSERT INTO node ( 22, (SELECT id FROM country WHERE iso_code = 'DE'), 'Reutlingen (KION plant)', - 'Frankfurt am Main International Airport (FRA), 60547 Frankfurt am Main, Germany', + 'Ernst-Wagner-Weg 1-5, 72766 Reutlingen', 'KWS', - false, + true, true, true, false, - 50.0379, - 8.5622 + 48.5639, + 9.2456 ); --- Node 23: EGD (Dinklage) +-- Node 23: Dinklage (KION plant) INSERT INTO node ( id, country_id, @@ -619,17 +612,17 @@ INSERT INTO node ( 23, (SELECT id FROM country WHERE iso_code = 'DE'), 'Dinklage (KION plant)', - 'Konrad-Zuse-Platz 8, 70173 Stuttgart, Germany', + 'In d. Bahler Heide 5, 49413 Dinklage', 'EGD', - false, + true, true, true, false, - 48.7771, - 9.1807 + 52.6736, + 8.1319 ); --- Node 24: CTT (Châtellerault) +-- Node 24: Châtellerault (KION plant) INSERT INTO node ( id, country_id, @@ -646,17 +639,17 @@ INSERT INTO node ( 24, (SELECT id FROM country WHERE iso_code = 'FR'), 'Châtellerault (KION plant)', - 'Cologne Bonn Airport, Kennedystraße, 51147 Cologne, Germany', + 'Rue de Touraine, 86530 Cenon-sur-Vienne, Frankreich', 'CTT', - false, + true, true, true, false, - 50.8660, - 7.1427 + 46.7762, + 0.5351 ); --- Node 25: LZZ (Luzzara) +-- Node 25: Luzzara INSERT INTO node ( id, country_id, @@ -673,17 +666,17 @@ INSERT INTO node ( 25, (SELECT id FROM country WHERE iso_code = 'IT'), 'Luzzara', - 'Berlin Brandenburg Airport Willy Brandt (BER), Melli-Beese-Ring 1, 12529 Schönefeld, Germany', + 'Via Bosa Est, 25, 42045 Luzzara RE, Italien', 'LZZ', - false, + true, true, true, false, - 52.3667, - 13.5033 + 44.9421, + 10.7078 ); --- Node 26: STR (Stříbro) +-- Node 26: Stříbro (KION plant) INSERT INTO node ( id, country_id, @@ -700,17 +693,17 @@ INSERT INTO node ( 26, (SELECT id FROM country WHERE iso_code = 'CZ'), 'Stříbro (KION plant)', - 'Munich Airport, Nordallee 25, 85356 Munich, Germany', + 'Ostrov u Stříbra 19, 349 01 Stříbro, Tschechien', 'STR', - false, + true, true, true, false, - 48.3538, - 11.7861 + 49.7002, + 13.035 ); --- Node 27: VOP (Nový Jičín) +-- Node 27: Nový Jičín (KION plant) INSERT INTO node ( id, country_id, @@ -727,17 +720,17 @@ INSERT INTO node ( 27, (SELECT id FROM country WHERE iso_code = 'CZ'), 'Nový Jičín (KION plant)', - 'Düsseldorf Airport, Flughafenstraße 120, 40474 Düsseldorf, Germany', + 'Dukelská 102, 742 42 Šenov u Nového Jičína, Tschechien', 'VOP', - false, + true, true, true, false, - 51.2895, - 6.7668 + 49.6153, + 17.9932 ); --- Node 28: KOL (Kołbaskowo) +-- Node 28: Kołbaskowo (KION plant) INSERT INTO node ( id, country_id, @@ -754,17 +747,17 @@ INSERT INTO node ( 28, (SELECT id FROM country WHERE iso_code = 'PL'), 'Kołbaskowo (KION plant)', - 'Nuremberg Airport, Flughafenstraße 100, 90411 Nuremberg, Germany', + 'Kołbaskowo 70, 72-001, Polen', 'KOL', - false, + true, true, true, false, - 49.4987, - 11.0669 + 53.3271, + 14.4288 ); --- Node 29: LiPo (Český Krumlov) +-- Node 29: Český Krumlov (KION plant) INSERT INTO node ( id, country_id, @@ -781,17 +774,17 @@ INSERT INTO node ( 29, (SELECT id FROM country WHERE iso_code = 'CZ'), 'Český Krumlov (KION plant)', - 'Bremen Airport, Flughafenallee 20, 28199 Bremen, Germany', + 'Tovární 118, 381 01 Český Krumlov 1, Tschechien', 'LiPo', - false, + true, true, true, false, - 53.0473, - 8.7867 + 48.8316, + 14.334 ); --- Node 30: Zbůch +-- Node 30: Urban Zbůch (Warehouse) INSERT INTO node ( id, country_id, @@ -808,17 +801,17 @@ INSERT INTO node ( 30, (SELECT id FROM country WHERE iso_code = 'CZ'), 'Urban Zbůch (Warehouse)', - 'Expeditors Germany GmbH, Building 606, Hamburg Airport, 22335 Hamburg, Germany', + 'Sokolská 35, 330 22 Zbůch, Tschechien', 'Zbůch', false, false, + false, true, - true, - 53.6304, - 9.9881 + 49.684, + 13.2244 ); --- Node 31: Urban Wörth +-- Node 31: Urban Wörth am Main (Warehouse) INSERT INTO node ( id, country_id, @@ -835,17 +828,17 @@ INSERT INTO node ( 31, (SELECT id FROM country WHERE iso_code = 'DE'), 'Urban Wörth am Main (Warehouse)', - 'Charles de Gaulle Airport, 95700 Roissy-en-France, France', + 'Landstraße 23A, 63939 Wörth am Main', 'Urban Wörth', false, false, + false, true, - true, - 49.0097, - 2.5479 + 49.7964, + 9.1612 ); --- Node 32: Urban AB +-- Node 32: Urban Aschaffenburg (Warehouse) INSERT INTO node ( id, country_id, @@ -862,17 +855,17 @@ INSERT INTO node ( 32, (SELECT id FROM country WHERE iso_code = 'DE'), 'Urban Aschaffenburg (Warehouse)', - 'Lyon–Saint-Exupéry Airport, 69125 Colombier-Saugnieu, France', + 'Germanenstraße 28, 63741 Aschaffenburg', 'Urban AB', false, false, + false, true, - true, - 45.7256, - 5.0811 + 49.9678, + 9.0947 ); --- Node 33: Pfaff Rade +-- Node 33: Pfaff Rade (Warehouse) INSERT INTO node ( id, country_id, @@ -889,17 +882,17 @@ INSERT INTO node ( 33, (SELECT id FROM country WHERE iso_code = 'DE'), 'Pfaff Rade (Warehouse)', - 'Marseille Provence Airport, 13727 Marignane, France', + 'Am Holz 3, 21629 Neu Wulmstorf', 'Pfaff Rade', false, false, + false, true, - true, - 43.4393, - 5.2214 + 53.374, + 9.7878 ); --- Node 34: Bat3 +-- Node 34: Geodis bâtiment 3 (Warehouse) INSERT INTO node ( id, country_id, @@ -916,17 +909,17 @@ INSERT INTO node ( 34, (SELECT id FROM country WHERE iso_code = 'FR'), 'Geodis bâtiment 3 (Warehouse)', - 'Toulouse–Blagnac Airport, 31703 Blagnac, France', + '115 Allée des érables, 86130 Dissay, France', 'Bat3', false, false, + false, true, - true, - 43.6291, - 1.3678 + 46.7027, + 0.4035 ); --- Node 35: Jeantet +-- Node 35: Jeantet Ouest (Warehouse) INSERT INTO node ( id, country_id, @@ -943,17 +936,17 @@ INSERT INTO node ( 35, (SELECT id FROM country WHERE iso_code = 'FR'), 'Jeantet Ouest (Warehouse)', - 'Nice Côte d''Azur Airport, 06206 Nice, France', + '12 Rue Dieudonné Costes et Mauric, 86100 Châtellerault, France', 'Jeantet', false, false, + false, true, - true, - 43.6658, - 7.2155 + 46.8403, + 0.5424 ); --- Node 36: Urban Rolo +-- Node 36: Urban Rolo I (Warehouse) INSERT INTO node ( id, country_id, @@ -970,17 +963,17 @@ INSERT INTO node ( 36, (SELECT id FROM country WHERE iso_code = 'IT'), 'Urban Rolo I (Warehouse)', - 'Bordeaux–Mérignac Airport, 33700 Mérignac, France', + 'Via Vincenzo Mari 3, 42047 Rolo RE, Italien', 'Urban Rolo', false, false, + false, true, - true, - 44.8283, - -0.7156 + 44.8712, + 10.8493 ); --- Node 37: CNSHA (Shanghai Port) +-- Node 37: Shanghai (Port) INSERT INTO node ( id, country_id, @@ -997,17 +990,17 @@ INSERT INTO node ( 37, (SELECT id FROM country WHERE iso_code = 'CN'), 'Shanghai (Port)', - 'Nantes Atlantique Airport, 44346 Bouguenais, France', + '9H6X+P28, Gangdian Rd, Pudong, Shanghai, China, 200131', 'CNSHA', false, false, false, true, - 47.1532, - -1.6107 + 31.364, + 121.5979 ); --- Node 38: CNTAO (Qingdao Port) +-- Node 38: Qingdao (Port) INSERT INTO node ( id, country_id, @@ -1024,17 +1017,17 @@ INSERT INTO node ( 38, (SELECT id FROM country WHERE iso_code = 'CN'), 'Qingdao (Port)', - 'Expeditors France, 7 Avenue Didier Daurat, 31400 Toulouse, France', + '2694+283, Weisi Rd, Huangdao, Qingdao, Shandong, China, 266500', 'CNTAO', false, false, false, true, - 43.6334, - 1.3822 + 36.0215, + 120.2149 ); --- Node 39: CNTGS (Tangshan Port) +-- Node 39: Tangshan (Port) INSERT INTO node ( id, country_id, @@ -1051,17 +1044,17 @@ INSERT INTO node ( 39, (SELECT id FROM country WHERE iso_code = 'CN'), 'Tangshan (Port)', - 'Milan Malpensa Airport, 21010 Ferno, Province of Varese, Italy', + 'China, Hebei, Tangshan, Laoting County, 海滨路 邮政编码: 063611', 'CNTGS', false, false, false, true, - 45.6306, - 8.7281 + 39.2166, + 118.9813 ); --- Node 40: CNXMN (Xiamen Port) +-- Node 40: Xiamen (Port) INSERT INTO node ( id, country_id, @@ -1078,17 +1071,17 @@ INSERT INTO node ( 40, (SELECT id FROM country WHERE iso_code = 'CN'), 'Xiamen (Port)', - 'Leonardo da Vinci International Airport, Via dell'' Aeroporto di Fiumicino, 320, 00054 Fiumicino, Metropolitan City of Rome, Italy', + 'C2WJ+WVC, Haicang District, Xiamen, Fujian, China, 361026', 'CNXMN', false, false, false, true, - 41.8003, - 12.2389 + 24.4486, + 118.0363 ); --- Node 41: CNYTN (Yantian Port) +-- Node 41: Yantian (Port) INSERT INTO node ( id, country_id, @@ -1105,17 +1098,17 @@ INSERT INTO node ( 41, (SELECT id FROM country WHERE iso_code = 'CN'), 'Yantian (Port)', - 'Naples International Airport, Via Pierluigi da Palestrina, 80144 Naples, Metropolitan City of Naples, Italy', + 'H7G8+G6G, Yantian District, Shenzhen, Guangdong Province, China', 'CNYTN', false, false, false, true, - 40.8860, - 14.2908 + 22.5765, + 114.2656 ); --- Node 42: INJNPT (Jawaharlal Nehru Port Trust) +-- Node 42: Jawaharlal Nehru Port Trust (Port) INSERT INTO node ( id, country_id, @@ -1132,17 +1125,17 @@ INSERT INTO node ( 42, (SELECT id FROM country WHERE iso_code = 'IN'), 'Jawaharlal Nehru Port Trust (Port)', - 'Turin Airport, Strada Aeroporto, 12, 10072 Caselle Torinese, Metropolitan City of Turin, Italy', + 'Sheva, Navi Mumbai, Juna Sheva, Maharashtra 400702, Indien', 'INJNPT', false, false, false, true, - 45.2008, - 7.6497 + 18.9337, + 72.9515 ); --- Node 43: DEHAM (Hamburg Port) +-- Node 43: Hamburg (Port) INSERT INTO node ( id, country_id, @@ -1159,17 +1152,17 @@ INSERT INTO node ( 43, (SELECT id FROM country WHERE iso_code = 'DE'), 'Hamburg (Port)', - 'Falcone-Borsellino Airport, 90045 Cinisi, Metropolitan City of Palermo, Italy', + 'Waltershofer Damm, 21129 Hamburg', 'DEHAM', false, false, false, true, - 38.1756, - 13.0919 + 53.5271, + 9.9297 ); --- Node 44: FRLEH (Le Havre Port) +-- Node 44: Le Havre (Port) INSERT INTO node ( id, country_id, @@ -1186,17 +1179,17 @@ INSERT INTO node ( 44, (SELECT id FROM country WHERE iso_code = 'FR'), 'Le Havre (Port)', - 'Bologna Guglielmo Marconi Airport, Via Triumvirato, 84, 40132 Bologna, Metropolitan City of Bologna, Italy', + 'Zone Port 2000, Route de l’Estuaire Port Sud, 76600 Le Havre, Frankreich', 'FRLEH', false, false, false, true, - 44.5354, - 11.2887 + 49.4632, + 0.1485 ); --- Node 45: BEZEE (Zeebrugge Port) +-- Node 45: Zeebrugge (Port) INSERT INTO node ( id, country_id, @@ -1213,17 +1206,17 @@ INSERT INTO node ( 45, (SELECT id FROM country WHERE iso_code = 'BE'), 'Zeebrugge (Port)', - 'Expeditors Italy s.r.l., Via Giovanni Keplero, 6, 20124 Milan, Metropolitan City of Milan, Italy', + 'Leopold II Laan, kaai 120, 8380 Brugge, Belgien', 'BEZEE', false, false, false, true, - 45.4972, - 9.2319 + 51.3457, + 3.1946 ); --- Node 46: INMAA (Chennai Port) +-- Node 46: Chennai (Port) INSERT INTO node ( id, country_id, @@ -1240,17 +1233,17 @@ INSERT INTO node ( 46, (SELECT id FROM country WHERE iso_code = 'IN'), 'Chennai (Port)', - 'Václav Havel Airport Prague, 160 08 Prague 6, Czechia', + 'Ground Chennai Port Trust Administrative Building, 1, Rajaji Salai, Chennai, Tamil Nadu 600001, Indien', 'INMAA', false, false, false, true, - 50.1008, - 14.2632 + 13.1104, + 80.2973 ); --- Node 47: HKMBP (Hong Kong Port) +-- Node 47: Hong Kong (Port) INSERT INTO node ( id, country_id, @@ -1267,17 +1260,17 @@ INSERT INTO node ( 47, (SELECT id FROM country WHERE iso_code = 'CN'), 'Hong Kong (Port)', - 'Brno–Tuřany Airport, 627 00 Brno-Tuřany, Czechia', + 'Tsing Yi, Hongkong', 'HKMBP', false, false, false, true, - 49.1513, - 16.6944 + 22.3407, + 114.1113 ); --- Node 48: ITGOA (Genoa Port) +-- Node 48: Genoa (Port) INSERT INTO node ( id, country_id, @@ -1294,17 +1287,17 @@ INSERT INTO node ( 48, (SELECT id FROM country WHERE iso_code = 'IT'), 'Genoa (Port)', - 'Leoš Janáček Airport Ostrava, 742 51 Mošnov, Czechia', + 'Viale Africa, 16149 Genova GE, Italien', 'ITGOA', false, false, false, true, - 49.6963, - 18.1111 + 44.4106, + 8.8823 ); --- Node 49: CNSZX (Shenzhen Port) +-- Node 49: Shenzhen (Port) INSERT INTO node ( id, country_id, @@ -1321,12 +1314,13 @@ INSERT INTO node ( 49, (SELECT id FROM country WHERE iso_code = 'CN'), 'Shenzhen (Port)', - 'Expeditors Czech Republic s.r.o., Tupolevova 665/17, 199 00 Prague 18-Letňany, Czechia', + 'H64R+VQ6, Yantian District, Shenzhen, Guangdong Province, China', 'CNSZX', false, false, false, true, - 50.1220, - 14.5169 - ); \ No newline at end of file + 22.558, + 114.2416 + ); + diff --git a/src/main/resources/master_data/04-predecessor-nodes.sql b/src/main/resources/master_data/04-predecessor-nodes.sql index b6b0781..21e3404 100644 --- a/src/main/resources/master_data/04-predecessor-nodes.sql +++ b/src/main/resources/master_data/04-predecessor-nodes.sql @@ -1,6 +1,5 @@ --- SQL INSERT-Statements für node_predecessor_chain und node_predecessor_entry --- Generiert aus Lastenheft_Requirements Appendix C_Vorknoten.csv --- 51 Predecessor-Ketten +-- Automatisch generierte SQL-Statements für Node Predecessor Chains +-- Generiert aus: pre_nodes.xlsx -- Predecessor Chain 1: CTT INSERT INTO node_predecessor_chain ( @@ -1179,4 +1178,4 @@ INSERT INTO node_predecessor_entry ( (SELECT id FROM node WHERE external_mapping_id = 'Shenzhen'), @chain_id_51, 1 - ); \ No newline at end of file + ); diff --git a/src/main/resources/master_data/08-data-containerrate.sql b/src/main/resources/master_data/08-data-containerrate.sql new file mode 100644 index 0000000..6fddd86 --- /dev/null +++ b/src/main/resources/master_data/08-data-containerrate.sql @@ -0,0 +1,1332 @@ +SET @validity_period_id = ( + SELECT id + FROM validity_period + WHERE state = 'VALID' + AND start_date <= NOW() + AND (end_date IS NULL OR end_date > NOW()) + LIMIT 1 +); + +-- Generated SQL INSERT statements for container rates +-- Generated from shipping_data.csv + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Xiamen'), + (SELECT id FROM node WHERE external_mapping_id = 'CNXMN'), + 'ROAD', + 118.00, + 129.00, + 131.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Shanghai'), + (SELECT id FROM node WHERE external_mapping_id = 'CNSHA'), + 'ROAD', + 127.00, + 144.00, + 147.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Hangzhou'), + (SELECT id FROM node WHERE external_mapping_id = 'CNSHA'), + 'ROAD', + 372.00, + 535.00, + 562.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Yangzhong'), + (SELECT id FROM node WHERE external_mapping_id = 'CNSHA'), + 'ROAD', + 394.00, + 570.00, + 599.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Taicang'), + (SELECT id FROM node WHERE external_mapping_id = 'CNSHA'), + 'ROAD', + 183.00, + 263.00, + 276.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Jinjiang'), + (SELECT id FROM node WHERE external_mapping_id = 'CNTAO'), + 'ROAD', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Qingdao'), + (SELECT id FROM node WHERE external_mapping_id = 'CNTAO'), + 'ROAD', + 100.00, + 160.00, + 170.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Fuqin'), + (SELECT id FROM node WHERE external_mapping_id = 'CNTGS'), + 'ROAD', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Linfen'), + (SELECT id FROM node WHERE external_mapping_id = 'CNYTN'), + 'ROAD', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Yantian'), + (SELECT id FROM node WHERE external_mapping_id = 'CNYTN'), + 'ROAD', + 264.00, + 423.00, + 449.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Bangalore'), + (SELECT id FROM node WHERE external_mapping_id = 'INMAA'), + 'ROAD', + 546.00, + 774.00, + 828.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Pune'), + (SELECT id FROM node WHERE external_mapping_id = 'INMAA'), + 'ROAD', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'JJ'), + (SELECT id FROM node WHERE external_mapping_id = 'ExM'), + 'ROAD', + 250.00, + 400.00, + 425.00, + 2, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'LX'), + (SELECT id FROM node WHERE external_mapping_id = 'ExM'), + 'ROAD', + 250.00, + 400.00, + 425.00, + 2, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Aurangabad'), + (SELECT id FROM node WHERE external_mapping_id = 'INJNPT'), + 'ROAD', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'Chennai'), + (SELECT id FROM node WHERE external_mapping_id = 'INMAA'), + 'ROAD', + 626.00, + 901.00, + 965.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNSHA'), + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + 'SEA', + 13803.00, + 22485.00, + 23463.00, + 24, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNTAO'), + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + 'SEA', + 14216.00, + 23146.00, + 24148.00, + 25, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNTGS'), + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + 'SEA', + 14697.00, + 23915.00, + 24963.00, + 26, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNXMN'), + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + 'SEA', + 13901.00, + 22642.00, + 23629.00, + 24, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNYTN'), + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + 'SEA', + 13779.00, + 22447.00, + 23422.00, + 24, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'INMAA'), + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + 'SEA', + 10355.00, + 16968.00, + 17702.00, + 18, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'ExM'), + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + 'SEA', + 1200.00, + 1920.00, + 2040.00, + 15, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'INJNPT'), + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + 'SEA', + 10311.00, + 16897.00, + 17627.00, + 18, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNSHA'), + (SELECT id FROM node WHERE external_mapping_id = 'BEZEE'), + 'SEA', + 13795.00, + 22472.00, + 23448.00, + 24, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNTAO'), + (SELECT id FROM node WHERE external_mapping_id = 'BEZEE'), + 'SEA', + 14208.00, + 23133.00, + 24134.00, + 25, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNTGS'), + (SELECT id FROM node WHERE external_mapping_id = 'BEZEE'), + 'SEA', + 14689.00, + 23902.00, + 24950.00, + 26, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNXMN'), + (SELECT id FROM node WHERE external_mapping_id = 'BEZEE'), + 'SEA', + 13893.00, + 22629.00, + 23615.00, + 24, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNYTN'), + (SELECT id FROM node WHERE external_mapping_id = 'BEZEE'), + 'SEA', + 13771.00, + 22434.00, + 23408.00, + 24, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'INMAA'), + (SELECT id FROM node WHERE external_mapping_id = 'BEZEE'), + 'SEA', + 10347.00, + 16955.00, + 17688.00, + 18, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'ExM'), + (SELECT id FROM node WHERE external_mapping_id = 'BEZEE'), + 'SEA', + 1200.00, + 1920.00, + 2040.00, + 15, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'INJNPT'), + (SELECT id FROM node WHERE external_mapping_id = 'BEZEE'), + 'SEA', + 10303.00, + 16885.00, + 17614.00, + 18, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNSHA'), + (SELECT id FROM node WHERE external_mapping_id = 'FRLEH'), + 'SEA', + 13691.00, + 22306.00, + 23270.00, + 24, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNTAO'), + (SELECT id FROM node WHERE external_mapping_id = 'FRLEH'), + 'SEA', + 14104.00, + 22966.00, + 23956.00, + 25, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNTGS'), + (SELECT id FROM node WHERE external_mapping_id = 'FRLEH'), + 'SEA', + 14585.00, + 23736.00, + 24772.00, + 26, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNXMN'), + (SELECT id FROM node WHERE external_mapping_id = 'FRLEH'), + 'SEA', + 13789.00, + 22462.00, + 23437.00, + 24, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNYTN'), + (SELECT id FROM node WHERE external_mapping_id = 'FRLEH'), + 'SEA', + 13667.00, + 22267.00, + 23230.00, + 24, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'INMAA'), + (SELECT id FROM node WHERE external_mapping_id = 'FRLEH'), + 'SEA', + 9795.00, + 16072.00, + 16765.00, + 17, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'ExM'), + (SELECT id FROM node WHERE external_mapping_id = 'FRLEH'), + 'SEA', + 1200.00, + 1920.00, + 2040.00, + 15, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'INJNPT'), + (SELECT id FROM node WHERE external_mapping_id = 'FRLEH'), + 'SEA', + 9751.00, + 16002.00, + 16692.00, + 17, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNSHA'), + (SELECT id FROM node WHERE external_mapping_id = 'ITGOA'), + 'SEA', + 12430.00, + 20288.00, + 21182.00, + 22, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNTAO'), + (SELECT id FROM node WHERE external_mapping_id = 'ITGOA'), + 'SEA', + 12843.00, + 20949.00, + 21871.00, + 22, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNTGS'), + (SELECT id FROM node WHERE external_mapping_id = 'ITGOA'), + 'SEA', + 13324.00, + 21718.00, + 22689.00, + 23, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNXMN'), + (SELECT id FROM node WHERE external_mapping_id = 'ITGOA'), + 'SEA', + 12528.00, + 20445.00, + 21346.00, + 22, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'CNYTN'), + (SELECT id FROM node WHERE external_mapping_id = 'ITGOA'), + 'SEA', + 12406.00, + 20250.00, + 21142.00, + 22, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'INMAA'), + (SELECT id FROM node WHERE external_mapping_id = 'ITGOA'), + 'SEA', + 6069.00, + 9710.00, + 10417.00, + 11, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'ExM'), + (SELECT id FROM node WHERE external_mapping_id = 'ITGOA'), + 'SEA', + 1200.00, + 1920.00, + 2040.00, + 15, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'INJNPT'), + (SELECT id FROM node WHERE external_mapping_id = 'ITGOA'), + 'SEA', + 6025.00, + 9640.00, + 10342.00, + 11, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'Zbůch'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'Pfaff Rade'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'KOL'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'Urban Wörth'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'Bat3'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'Urban AB'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'Jeantet'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'AB'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'FGG'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'KWS'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'EGD'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'HH'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'LiPo'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'STR'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'VOP'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'CTT'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'LZZ'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + +INSERT INTO container_rate ( + from_node_id, + to_node_id, + container_rate_type, + rate_teu, + rate_feu, + rate_hc, + lead_time, + validity_period_id +) VALUES ( + (SELECT id FROM node WHERE external_mapping_id = 'DEHAM'), + (SELECT id FROM node WHERE external_mapping_id = 'Urban Rolo'), + 'POST_RUN', + 250.00, + 400.00, + 425.00, + 1, + @validity_period_id + ); + diff --git a/src/main/resources/master_data/excel_to_sql_converter.py b/src/main/resources/master_data/excel_to_sql_converter_materials.py similarity index 100% rename from src/main/resources/master_data/excel_to_sql_converter.py rename to src/main/resources/master_data/excel_to_sql_converter_materials.py diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 7ced920..99b88fe 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `property_set` `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `start_date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `end_date` TIMESTAMP NULL, - `state` CHAR(8) NOT NULL, + `state` CHAR(8) NOT NULL, CONSTRAINT `chk_property_state_values` CHECK (`state` IN ('DRAFT', 'VALID', 'INVALID', 'EXPIRED')), CONSTRAINT `chk_property_date_range` CHECK (`end_date` IS NULL OR `end_date` > `start_date`), INDEX `idx_dates` (`start_date`, `end_date`), @@ -124,8 +124,8 @@ CREATE TABLE IF NOT EXISTS `sys_user_node` `country_id` INT NOT NULL, `name` VARCHAR(254) NOT NULL, `address` VARCHAR(500) NOT NULL, - `geo_lat` DECIMAL(7, 4) CHECK (geo_lat BETWEEN -90 AND 90), - `geo_lng` DECIMAL(7, 4) CHECK (geo_lng BETWEEN -180 AND 180), + `geo_lat` DECIMAL(8, 4) CHECK (geo_lat BETWEEN -90 AND 90), + `geo_lng` DECIMAL(8, 4) CHECK (geo_lng BETWEEN -180 AND 180), `is_deprecated` BOOLEAN DEFAULT FALSE, FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`), FOREIGN KEY (`country_id`) REFERENCES `country` (`id`) @@ -141,11 +141,11 @@ CREATE TABLE IF NOT EXISTS node address VARCHAR(500) NOT NULL, external_mapping_id VARCHAR(32), predecessor_required BOOLEAN NOT NULL DEFAULT FALSE, - is_destination BOOLEAN NOT NULL, - is_source BOOLEAN NOT NULL, - is_intermediate BOOLEAN NOT NULL, - geo_lat DECIMAL(7, 4) CHECK (geo_lat BETWEEN -90 AND 90), - geo_lng DECIMAL(7, 4) CHECK (geo_lng BETWEEN -180 AND 180), + is_destination BOOLEAN NOT NULL, + is_source BOOLEAN NOT NULL, + is_intermediate BOOLEAN NOT NULL, + geo_lat DECIMAL(8, 4) CHECK (geo_lat BETWEEN -90 AND 90), + geo_lng DECIMAL(8, 4) CHECK (geo_lng BETWEEN -180 AND 180), updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_deprecated BOOLEAN NOT NULL DEFAULT FALSE, FOREIGN KEY (country_id) REFERENCES country (id), @@ -190,17 +190,17 @@ CREATE TABLE IF NOT EXISTS distance_matrix id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, from_node_id INT NOT NULL, to_node_id INT NOT NULL, - from_geo_lat DECIMAL(7, 4) CHECK (from_geo_lat BETWEEN -90 AND 90), - from_geo_lng DECIMAL(7, 4) CHECK (from_geo_lng BETWEEN -180 AND 180), - to_geo_lat DECIMAL(7, 4) CHECK (to_geo_lat BETWEEN -90 AND 90), - to_geo_lng DECIMAL(7, 4) CHECK (to_geo_lng BETWEEN -180 AND 180), + from_geo_lat DECIMAL(8, 4) CHECK (from_geo_lat BETWEEN -90 AND 90), + from_geo_lng DECIMAL(8, 4) CHECK (from_geo_lng BETWEEN -180 AND 180), + to_geo_lat DECIMAL(8, 4) CHECK (to_geo_lat BETWEEN -90 AND 90), + to_geo_lng DECIMAL(8, 4) CHECK (to_geo_lng BETWEEN -180 AND 180), distance DECIMAL(15, 2) NOT NULL COMMENT 'travel distance between the two nodes in meters', updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - state CHAR(10) NOT NULL, + state CHAR(10) NOT NULL, FOREIGN KEY (from_node_id) REFERENCES node (id), FOREIGN KEY (to_node_id) REFERENCES node (id), CONSTRAINT `chk_distance_matrix_state` CHECK (`state` IN - ('VALID', 'STALE')), + ('VALID', 'STALE')), INDEX idx_from_to_nodes (from_node_id, to_node_id) ); @@ -211,7 +211,7 @@ CREATE TABLE IF NOT EXISTS validity_period id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, start_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, end_date TIMESTAMP DEFAULT NULL, - state CHAR(8) NOT NULL CHECK (state IN ('DRAFT', 'VALID', 'INVALID', 'EXPIRED')), + state CHAR(8) NOT NULL CHECK (state IN ('DRAFT', 'VALID', 'INVALID', 'EXPIRED')), CONSTRAINT `chk_validity_date_range` CHECK (`end_date` IS NULL OR `end_date` > `start_date`) ); @@ -220,7 +220,7 @@ CREATE TABLE IF NOT EXISTS container_rate id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, from_node_id INT NOT NULL, to_node_id INT NOT NULL, - container_rate_type CHAR(8) CHECK (container_rate_type IN ('RAIL', 'SEA', 'POST-RUN', 'ROAD')), + container_rate_type CHAR(8) CHECK (container_rate_type IN ('RAIL', 'SEA', 'POST_RUN', 'ROAD')), rate_teu DECIMAL(15, 2) NOT NULL COMMENT 'rate for 20ft container in EUR', rate_feu DECIMAL(15, 2) NOT NULL COMMENT 'rate for 40ft container in EUR', rate_hc DECIMAL(15, 2) NOT NULL COMMENT 'rate for 40ft HQ container in EUR', @@ -273,11 +273,11 @@ CREATE TABLE IF NOT EXISTS packaging_dimension `content_unit_count` INT UNSIGNED NOT NULL COMMENT 'how many units are contained in packaging (if there is a child packaging this references to the child packaging, otherwise this references a single unit)', `is_deprecated` BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT `chk_packaging_dimension_type_values` CHECK (`type` IN - ('SHU', 'HU')), + ('SHU', 'HU')), CONSTRAINT `chk_packaging_dimension_displayed_dimension_unit` CHECK (`displayed_dimension_unit` IN - ('MM', 'CM', 'M')), + ('MM', 'CM', 'M')), CONSTRAINT `chk_packaging_dimension_displayed_weight_unit` CHECK (`displayed_weight_unit` IN - ('G', 'KG')) + ('G', 'KG')) ); CREATE TABLE IF NOT EXISTS packaging @@ -332,19 +332,19 @@ CREATE TABLE IF NOT EXISTS premise material_id INT NOT NULL, supplier_node_id INT, user_supplier_node_id INT, - geo_lat DECIMAL(7, 4) CHECK (geo_lat BETWEEN -90 AND 90), - geo_lng DECIMAL(7, 4) CHECK (geo_lng BETWEEN -180 AND 180), + geo_lat DECIMAL(8, 4) CHECK (geo_lat BETWEEN -90 AND 90), + geo_lng DECIMAL(8, 4) CHECK (geo_lng BETWEEN -180 AND 180), country_id INT NOT NULL, packaging_id INT DEFAULT NULL, user_id INT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - material_cost DECIMAL(15, 2) COMMENT 'aka MEK_A in EUR', + material_cost DECIMAL(15, 2) DEFAULT NULL COMMENT 'aka MEK_A in EUR', is_fca_enabled BOOLEAN DEFAULT FALSE, - oversea_share DECIMAL(7, 4), - hs_code CHAR(8), - tariff_rate DECIMAL(7, 4), - state CHAR(10) NOT NULL DEFAULT 'DRAFT', + oversea_share DECIMAL(8, 4) DEFAULT NULL, + hs_code CHAR(8) DEFAULT NULL, + tariff_rate DECIMAL(8, 4) DEFAULT NULL, + state CHAR(10) NOT NULL DEFAULT 'DRAFT', individual_hu_length INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)', individual_hu_height INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)', individual_hu_width INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)', @@ -380,13 +380,13 @@ CREATE TABLE IF NOT EXISTS premise_destination annual_amount INT UNSIGNED NOT NULL COMMENT 'annual amount in single pieces', destination_node_id INT NOT NULL, is_d2d BOOLEAN DEFAULT FALSE, - rate_d2d DECIMAL(15, 2) DEFAULT NULL, - lead_time_d2d INT UNSIGNED NOT NULL, - repacking_cost DECIMAL(15, 2) DEFAULT NULL, - handling_cost DECIMAL(15, 2) DEFAULT NULL, - disposal_cost DECIMAL(15, 2) DEFAULT NULL, - geo_lat DECIMAL(7, 4) CHECK (geo_lat BETWEEN -90 AND 90), - geo_lng DECIMAL(7, 4) CHECK (geo_lng BETWEEN -180 AND 180), + rate_d2d DECIMAL(15, 2) DEFAULT NULL CHECK (rate_d2d >= 0), + lead_time_d2d INT UNSIGNED DEFAULT NULL, + repacking_cost DECIMAL(15, 2) DEFAULT NULL CHECK (repacking_cost >= 0), + handling_cost DECIMAL(15, 2) DEFAULT NULL CHECK (handling_cost >= 0), + disposal_cost DECIMAL(15, 2) DEFAULT NULL CHECK (disposal_cost >= 0), + geo_lat DECIMAL(8, 4) CHECK (geo_lat BETWEEN -90 AND 90), + geo_lng DECIMAL(8, 4) CHECK (geo_lng BETWEEN -180 AND 180), country_id INT NOT NULL, FOREIGN KEY (premise_id) REFERENCES premise (id), FOREIGN KEY (country_id) REFERENCES country (id), @@ -395,16 +395,6 @@ CREATE TABLE IF NOT EXISTS premise_destination INDEX idx_premise_id (premise_id) ); -CREATE TABLE IF NOT EXISTS premise_route -( - id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, - premise_destination_id INT NOT NULL, - is_fastest BOOLEAN DEFAULT FALSE, - is_cheapest BOOLEAN DEFAULT FALSE, - is_selected BOOLEAN DEFAULT FALSE, - FOREIGN KEY (premise_destination_id) REFERENCES premise_destination (id) -); - CREATE TABLE IF NOT EXISTS premise_route_node ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -416,8 +406,8 @@ CREATE TABLE IF NOT EXISTS premise_route_node is_destination BOOLEAN DEFAULT FALSE, is_intermediate BOOLEAN DEFAULT FALSE, is_source BOOLEAN DEFAULT FALSE, - geo_lat DECIMAL(7, 4) CHECK (geo_lat BETWEEN -90 AND 90), - geo_lng DECIMAL(7, 4) CHECK (geo_lng BETWEEN -180 AND 180), + geo_lat DECIMAL(8, 4) CHECK (geo_lat BETWEEN -90 AND 90), + geo_lng DECIMAL(8, 4) CHECK (geo_lng BETWEEN -180 AND 180), is_outdated BOOLEAN DEFAULT FALSE, FOREIGN KEY (node_id) REFERENCES node (id), FOREIGN KEY (country_id) REFERENCES country (id), @@ -427,6 +417,18 @@ CREATE TABLE IF NOT EXISTS premise_route_node CONSTRAINT `chk_node` CHECK (`user_node_id` IS NULL OR `node_id` IS NULL) ); + +CREATE TABLE IF NOT EXISTS premise_route +( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + premise_destination_id INT NOT NULL, + is_fastest BOOLEAN DEFAULT FALSE, + is_cheapest BOOLEAN DEFAULT FALSE, + is_selected BOOLEAN DEFAULT FALSE, + FOREIGN KEY (premise_destination_id) REFERENCES premise_destination (id) +); + + CREATE TABLE IF NOT EXISTS premise_route_section ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -435,22 +437,25 @@ CREATE TABLE IF NOT EXISTS premise_route_section to_route_node_id INT NOT NULL, list_position INT NOT NULL, transport_type CHAR(16) CHECK (transport_type IN - ('RAIL', 'SEA', 'ROAD', 'POST-RUN')), + ('RAIL', 'SEA', 'ROAD', 'POST_RUN')), rate_type CHAR(16) CHECK (rate_type IN ('CONTAINER', 'MATRIX')), is_pre_run BOOLEAN DEFAULT FALSE, is_main_run BOOLEAN DEFAULT FALSE, is_post_run BOOLEAN DEFAULT FALSE, is_outdated BOOLEAN DEFAULT FALSE, - FOREIGN KEY (premise_route_id) REFERENCES premise_route (id), + CONSTRAINT fk_premise_route_section_premise_route_id FOREIGN KEY (premise_route_id) REFERENCES premise_route (id), FOREIGN KEY (from_route_node_id) REFERENCES premise_route_node (id), FOREIGN KEY (to_route_node_id) REFERENCES premise_route_node (id), - CONSTRAINT chk_main_run CHECK ((transport_type = 'RAIL' OR transport_type = 'SEA') AND is_main_run IS TRUE), + CONSTRAINT chk_main_run CHECK ((transport_type != 'RAIL' AND transport_type != 'SEA') OR is_main_run IS TRUE), INDEX idx_premise_route_id (premise_route_id), INDEX idx_from_route_node_id (from_route_node_id), INDEX idx_to_route_node_id (to_route_node_id) ); + + + CREATE TABLE IF NOT EXISTS calculation_job ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -458,8 +463,8 @@ CREATE TABLE IF NOT EXISTS calculation_job calculation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, validity_period_id INT NOT NULL, property_set_id INT NOT NULL, - job_state CHAR(10) NOT NULL CHECK (job_state IN - ('CREATED', 'SCHEDULED', 'VALID', 'INVALID', 'EXCEPTION')), + job_state CHAR(10) NOT NULL CHECK (job_state IN + ('CREATED', 'SCHEDULED', 'VALID', 'INVALID', 'EXCEPTION')), user_id INT NOT NULL, FOREIGN KEY (premise_id) REFERENCES premise (id), FOREIGN KEY (validity_period_id) REFERENCES validity_period (id), @@ -499,11 +504,11 @@ CREATE TABLE IF NOT EXISTS calculation_job_destination -- custom custom_value DECIMAL(15, 2) NOT NULL,-- Zollwert, custom_duties DECIMAL(15, 2) NOT NULL,-- Zollabgaben, - tariff_rate DECIMAL(7, 4) NOT NULL,-- Zollsatz, + tariff_rate DECIMAL(8, 4) NOT NULL,-- Zollsatz, annual_custom_cost DECIMAL(15, 2) NOT NULL,-- Zollabgaben inkl. Einmalkosten, -- air freight risk - air_freight_share_max DECIMAL(7, 4) NOT NULL, - air_freight_share DECIMAL(7, 4) NOT NULL, + air_freight_share_max DECIMAL(8, 4) NOT NULL, + air_freight_share DECIMAL(8, 4) NOT NULL, air_freight_volumetric_weight DECIMAL(15, 2) NOT NULL, air_freight_weight DECIMAL(15, 2) NOT NULL, annual_air_freight_cost DECIMAL(15, 2) NOT NULL, @@ -517,7 +522,7 @@ CREATE TABLE IF NOT EXISTS calculation_job_destination layer_count INT UNSIGNED NOT NULL COMMENT 'number of layers per full container or truck', transport_weight_exceeded BOOLEAN DEFAULT FALSE COMMENT 'limiting factor: TRUE if weight limited or FALSE if volume limited', annual_transportation_cost DECIMAL(15, 2) NOT NULL COMMENT 'total annual transportation costs in EUR', - container_utilization DECIMAL(7, 4) NOT NULL, + container_utilization DECIMAL(8, 4) NOT NULL, transit_time_in_days INT UNSIGNED NOT NULL, safety_stock_in_days INT UNSIGNED NOT NULL, -- material cost @@ -536,7 +541,7 @@ CREATE TABLE IF NOT EXISTS calculation_job_route_section premise_route_section_id INT NOT NULL, calculation_job_destination_id INT NOT NULL, transport_type CHAR(16) CHECK (transport_type IN - ('RAIL', 'SEA', 'ROAD', 'POST-RUN', 'MATRIX', 'D2D')), + ('RAIL', 'SEA', 'ROAD', 'POST_RUN', 'MATRIX', 'D2D')), is_unmixed_price BOOLEAN DEFAULT FALSE, is_cbm_price BOOLEAN DEFAULT FALSE, is_weight_price BOOLEAN DEFAULT FALSE, diff --git a/src/test/java/de/avatic/lcc/controller/calculation/PremiseControllerIntegrationTest.java b/src/test/java/de/avatic/lcc/controller/calculation/PremiseControllerIntegrationTest.java new file mode 100644 index 0000000..33cacf9 --- /dev/null +++ b/src/test/java/de/avatic/lcc/controller/calculation/PremiseControllerIntegrationTest.java @@ -0,0 +1,1180 @@ +package de.avatic.lcc.controller.calculation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; +import de.avatic.lcc.dto.generic.MaterialDTO; +import de.avatic.lcc.model.nodes.Location; +import de.avatic.lcc.model.premises.Premise; +import de.avatic.lcc.model.premises.PremiseState; +import de.avatic.lcc.model.premises.route.Destination; +import de.avatic.lcc.model.utils.DimensionUnit; +import de.avatic.lcc.model.utils.WeightUnit; +import de.avatic.lcc.repositories.premise.DestinationRepository; +import de.avatic.lcc.repositories.premise.PremiseRepository; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@Import(PremiseControllerTestData.class) + +public class PremiseControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + protected JdbcTemplate jdbcTemplate; + + @Autowired + private PremiseControllerTestData premiseTestData; + + + @Nested + @Sql(scripts = {"classpath:master_data/alldata.sql", "classpath:master_data/users.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) + @Sql(scripts = {"classpath:master_data/users-cleanup.sql", "classpath:master_data/alldata-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS) + @Sql(scripts = {"classpath:master_data/premises.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/premises-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + class ViewPremisesTests { + + // Test for GET /api/calculation/view + @Test + @DisplayName("GET /api/calculation/view - happy path (no filter)") + public void viewMaterialTest() throws Exception { + mockMvc.perform(get("/api/calculation/view")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$", isA(List.class))); + + //TODO user login not implemented. user with id 1 is returned at the moment + } + + + @Test + @DisplayName("GET /api/calculation/view - happy path (filtering part number)") + public void viewMaterialFilterTest() throws Exception { + mockMvc.perform(get("/api/calculation/view").param("filter", "28152640129")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$", isA(List.class))) + .andExpect(jsonPath("$[*].material.part_number", + everyItem(equalTo("28152640129")))); + + //TODO user login not implemented. user with id 1 is returned at the moment + } + + @Test + @DisplayName("GET /api/calculation/view - happy path (filtering supplier name)") + public void viewMaterialFilterSupplierTest() throws Exception { + mockMvc.perform(get("/api/calculation/view").param("filter", "My Supplier 1")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$", isA(List.class))) + .andExpect(jsonPath("$[*].supplier.name").value(everyItem(equalTo("My Supplier 1")))); + + //TODO user login not implemented. user with id 1 is returned at the moment + } + + //TODO filter done, archived, deleted, etc. + // filter user id (check that this only works for admin) + // material name + // pagination + // none happy path: negative pages, user id as non admin + } + + @Nested + @Sql(scripts = {"classpath:master_data/alldata.sql", "classpath:master_data/users.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) + @Sql(scripts = {"classpath:master_data/users-cleanup.sql", "classpath:master_data/alldata-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS) + @Sql(scripts = {"classpath:master_data/premises.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/premises-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + class SearchMaterialTests { + + // Test for GET /api/calculation/search + @Test + @DisplayName("GET /api/calculation/search - happy path (single)") + public void searchSingleMaterialTest() throws Exception { + mockMvc.perform(get("/api/calculation/search") + .param("search", "28152640129")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$.materials", isA(List.class))) + .andExpect(jsonPath("$.materials", hasSize(1))) + .andExpect(jsonPath("$.supplier", hasSize(1))) + .andExpect(jsonPath("$.user_supplier", hasSize(1))) + .andExpect(jsonPath("$.user_supplier[?(@.name == 'My Supplier 1')]").exists()) + .andExpect(jsonPath("$.user_supplier[?(@.name == 'My Supplier 2')]").isEmpty()); + } + + + @Test + @DisplayName("GET /api/calculation/search - happy path (muliple)") + public void searchMultiMaterialTest() throws Exception { + mockMvc.perform(get("/api/calculation/search") + .param("search", "28152640129 Material 4222640803 bla4222640104bla bla")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$.materials", isA(List.class))) + .andExpect(jsonPath("$.materials", hasSize(2))) + .andExpect(jsonPath("$.supplier", hasSize(1))) + .andExpect(jsonPath("$.user_supplier", hasSize(1))) + .andExpect(jsonPath("$.user_supplier[?(@.name == 'My Supplier 1')]").exists()) + .andExpect(jsonPath("$.supplier[?(@.name == 'Linde (China) Forklift Truck (Supplier)')]").exists()) + .andExpect(jsonPath("$.materials[?(@.part_number == '28152640129')]").exists()) + .andExpect(jsonPath("$.materials[?(@.part_number == '4222640803')]").exists()); + } + + //TODO Test with faulty param + + } + + /** + * + *

Testcases

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Testfall #Descr.ParamsUserResult
1Copies COMPLETED with lowest agematerial_ids: 1, supplier_ids: 1, user_supplier_ids: null, from_scretch: false1New Premise with Data from Premise #1
2Recycles DRAFT from same usermaterial_ids: 2, supplier_ids: 1, user_supplier_ids: null, from_scretch: false1Returns Premise # 3
3Recycles COMPLETED from other usermaterial_ids: 3, supplier_ids: 1, user_supplier_ids: null, from_scretch: false1New Premise with Data from Premise #7
4Creates new Premise if no old datamaterial_ids: 4, supplier_ids: 1, user_supplier_ids: null, from_scretch: false1New Premise without Data
5Uses UserSupplier DRAFTmaterial_ids: 1, supplier_ids: null, user_supplier_ids: 1, from_scretch: false1Returns Premise # 8
6Access Violation UserSuppliermaterial_ids: 1, supplier_ids: null, user_supplier_ids: 2, from_scretch: false1Returns access violation
7From Scratch behavior testmaterial_ids: [1,2,4], supplier_ids: 1, user_supplier_ids: null, from_scretch: true1Resets/deletes Premise #3, Creates new Premise for Material 1
8Faulty material idmaterial_ids: -1, supplier_ids: 1, user_supplier_ids: null, from_scretch: false1400
9Faulty supplier idmaterial_ids: 1, supplier_ids: -1, user_supplier_ids: null, from_scretch: false1400
10Faulty supplier idmaterial_ids: 1, supplier_ids: null, user_supplier_ids: -1, from_scretch: false1400
+ * + * + *

Preconfigured Premises

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Premise #Material IDNode IDUser Node IDStateAgeUser
128152640129LXCOMPLETED1 MonthJohn Doe
228152640129LXCOMPLETED2 MonthJohn Doe
38222640822LXDRAFT1 MonthJohn Doe
48222640822LXCOMPLETED1 MonthJohn Doe
58222640822LXDRAFT1 MonthSarah Smith
63064540201LXDRAFT1 MonthSarah Smith
73064540201LXCOMPLETED1 MonthSarah Smith
828152640129My Supplier 1DRAFT1 MonthJohn Doe
928152640129My Supplier 2COMPLETED1 MonthSarah Smith
+ * + */ + @Nested + @Sql(scripts = {"classpath:master_data/alldata.sql", "classpath:master_data/users.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) + @Sql(scripts = {"classpath:master_data/users-cleanup.sql", "classpath:master_data/alldata-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS) + @Sql(scripts = {"classpath:master_data/premises.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/premises-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + class CreatePremiseTests { + + @BeforeEach + void setUp() { + premiseTestData.generateData(); + } + + @AfterEach + void cleanUp() { + premiseTestData.deleteAll(); + } + + + @Test + @DisplayName("POST /api/calculation/create - Copies COMPLETED with lowest age") + public void createPremiseCase1() throws Exception { + + + var supplierId = getSupplierIdByExternalMappingId("LX"); + var premisesBeforeCreate = getPremisesFromDb(); + var materialId = getMaterialIdByPartNumber("28152640129"); + + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", materialId.toString()); + params.add("supplier", supplierId.toString()); + params.add("from_scratch", "false"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$[?(@.material.part_number=='28152640129')].handling_unit.content_unit_count", hasItem(2))) // content_unit_count is in the newest premise set to 2 + .andExpect(jsonPath("$", hasSize(1))) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + + var dtos = objectMapper.readValue(result.getResponse().getContentAsString(), PremiseDetailDTO[].class); + assertThat(premisesBeforeCreate.stream().map(Premise::getId).toList()).asInstanceOf(InstanceOfAssertFactories.LIST).doesNotContain(dtos[0].getId()); + assertThat(premisesAfterCreate.stream().map(Premise::getId).toList()).asInstanceOf(InstanceOfAssertFactories.LIST).contains(dtos[0].getId()); + + assertThat(premisesAfterCreate.stream().map(Premise::getId).toList()).asInstanceOf(InstanceOfAssertFactories.LIST).containsAll(premisesBeforeCreate.stream().map(Premise::getId).toList()); + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesBeforeCreate.size() + 1); + + + // check that destinations are copied from completed. + assertThat(dtos[0].getDestinations().getFirst().getDestinationNode().getName()).isEqualTo("Aschaffenburg (KION plant)"); + + assertThat(dtos[0].getDestinations().getFirst().getRoutes().getFirst().getTransitNodes().get(0).getName()).isEqualTo("Linde (China) Forklift Truck (Supplier)"); + assertThat(dtos[0].getDestinations().getFirst().getRoutes().getFirst().getTransitNodes().get(1).getName()).isEqualTo("Xiamen (Port)"); + assertThat(dtos[0].getDestinations().getFirst().getRoutes().get(0).getTransitNodes().get(2).getName()).isEqualTo("Hamburg (Port)"); + assertThat(dtos[0].getDestinations().getFirst().getRoutes().get(0).getTransitNodes().get(3).getName()).isEqualTo("Aschaffenburg (KION plant)"); + + assertThat(dtos[0].getDestinations().getFirst().getRoutes().get(1).getTransitNodes().get(0).getName()).isEqualTo("Linde (China) Forklift Truck (Supplier)"); + assertThat(dtos[0].getDestinations().getFirst().getRoutes().get(1).getTransitNodes().get(1).getName()).isEqualTo("Xiamen (Port)"); + assertThat(dtos[0].getDestinations().getFirst().getRoutes().get(1).getTransitNodes().get(2).getName()).isEqualTo("Zeebrugge (Port)"); + assertThat(dtos[0].getDestinations().getFirst().getRoutes().get(1).getTransitNodes().get(3).getName()).isEqualTo("Aschaffenburg (KION plant)"); + + } + + @Test + @DisplayName("POST /api/calculation/create - Recycles DRAFT from same user") + public void createPremiseCase2() throws Exception { + + var supplierId = getSupplierIdByExternalMappingId("LX"); + var premisesBeforeCreate = getPremisesFromDb(); + var materialId = getMaterialIdByPartNumber("8222640822"); + + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", materialId.toString()); + params.add("supplier", supplierId.toString()); + params.add("from_scratch", "false"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$[?(@.material.part_number=='8222640822')].handling_unit.content_unit_count", hasItem(3))) // content_unit_count is in the correct permise set to 3 + .andExpect(jsonPath("$", hasSize(1))) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + + var dtos = objectMapper.readValue(result.getResponse().getContentAsString(), PremiseDetailDTO[].class); + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesAfterCreate.size()); + + // check that destinations are copied from completed. + assertThat(dtos[0].getDestinations().getFirst().getDestinationNode().getName()).isEqualTo("Châtellerault (KION plant)"); + + } + + @Test + @DisplayName("POST /api/calculation/create - Recycles COMPLETED from other user") + public void createPremiseCase3() throws Exception { + var supplierId = getSupplierIdByExternalMappingId("LX"); + var premisesBeforeCreate = getPremisesFromDb(); + var materialId = getMaterialIdByPartNumber("3064540201"); + + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", materialId.toString()); + params.add("supplier", supplierId.toString()); + params.add("from_scratch", "false"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$[?(@.material.part_number=='3064540201')].handling_unit.content_unit_count", hasItem(4))) // content_unit_count is in the correct premise set to 4 + .andExpect(jsonPath("$", hasSize(1))) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + + var dtos = objectMapper.readValue(result.getResponse().getContentAsString(), PremiseDetailDTO[].class); + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesBeforeCreate.size() + 1); + + // check that destinations are copied from completed. + assertThat(dtos[0].getDestinations()).asInstanceOf(InstanceOfAssertFactories.LIST).isEmpty(); + } + + @Test + @DisplayName("POST /api/calculation/create - Creates new Premise if no old data") + public void createPremiseCase4() throws Exception { + var supplierId = getSupplierIdByExternalMappingId("LX"); + var premisesBeforeCreate = getPremisesFromDb(); + var materialId = getMaterialIdByPartNumber("8212640113"); + + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", materialId.toString()); + params.add("supplier", supplierId.toString()); + params.add("from_scratch", "false"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$[?(@.material.part_number=='8212640113')].handling_unit.content_unit_count", hasItem(54))) // content_unit_count is the data from the packaging database. + .andExpect(jsonPath("$", hasSize(1))) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + + var dtos = objectMapper.readValue(result.getResponse().getContentAsString(), PremiseDetailDTO[].class); + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesBeforeCreate.size() + 1); + + // check that destinations are copied from completed. + assertThat(dtos[0].getDestinations()).asInstanceOf(InstanceOfAssertFactories.LIST).isEmpty(); + + } + + @Test + @DisplayName("POST /api/calculation/create - Uses UserSupplier DRAFT") + public void createPremiseCase5() throws Exception { + var supplierId = getUserSupplierIdByName("My Supplier 1"); + var premisesBeforeCreate = getPremisesFromDb(); + var materialId = getMaterialIdByPartNumber("28152640129"); + + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", materialId.toString()); + params.add("user_supplier", supplierId.toString()); + params.add("from_scratch", "false"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$[?(@.material.part_number=='28152640129')].handling_unit.content_unit_count", hasItem(8))) // content_unit_count is the data from the packaging database. + .andExpect(jsonPath("$", hasSize(1))) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + + var dtos = objectMapper.readValue(result.getResponse().getContentAsString(), PremiseDetailDTO[].class); + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesBeforeCreate.size()); + + // check that destinations are copied from completed. + assertThat(dtos[0].getDestinations()).asInstanceOf(InstanceOfAssertFactories.LIST).isEmpty(); + } + + @Test + @DisplayName("POST /api/calculation/create - Access Violation UserSupplier") + public void createPremiseCase6() throws Exception { + var supplierId = getUserSupplierIdByName("My Supplier 2"); + var premisesBeforeCreate = getPremisesFromDb(); + var materialId = getMaterialIdByPartNumber("28152640129"); + + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", materialId.toString()); + params.add("user_supplier", supplierId.toString()); + params.add("from_scratch", "false"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isForbidden()) + .andDo(print()) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesBeforeCreate.size()); + + } + + @Test + @DisplayName("POST /api/calculation/create - From Scratch behavior test") + public void createPremiseCase7() throws Exception { + var supplierId = getSupplierIdByExternalMappingId("LX"); + var materialId1 = getMaterialIdByPartNumber("28152640129"); + var materialId2 = getMaterialIdByPartNumber("8222640822"); + var materialId4 = getMaterialIdByPartNumber("8212640113"); + var premisesBeforeCreate = getPremisesFromDb(); + + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", materialId1.toString()); + params.add("material", materialId2.toString()); + params.add("material", materialId4.toString()); + params.add("supplier", supplierId.toString()); + params.add("from_scratch", "true"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$[?(@.material.part_number=='28152640129')].handling_unit.content_unit_count", hasItem(1))) // content_unit_count is the data from the packaging database. + .andExpect(jsonPath("$[?(@.material.part_number=='8222640822')].handling_unit.content_unit_count", hasItem(630))) // content_unit_count is the data from the packaging database. + .andExpect(jsonPath("$[?(@.material.part_number=='8212640113')].handling_unit.content_unit_count", hasItem(54))) // content_unit_count is the data from the packaging database. + .andExpect(jsonPath("$", hasSize(3))) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + + var dtos = objectMapper.readValue(result.getResponse().getContentAsString(), PremiseDetailDTO[].class); + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesBeforeCreate.size() + 2); + + // check that destinations are copied from completed. + assertThat(dtos[0].getDestinations()).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(1); + assertThat(dtos[1].getDestinations()).asInstanceOf(InstanceOfAssertFactories.LIST).isEmpty(); + assertThat(dtos[2].getDestinations()).asInstanceOf(InstanceOfAssertFactories.LIST).isEmpty(); + } + + @Test + @DisplayName("POST /api/calculation/create - Faulty material id") + public void createPremiseCase8() throws Exception { + + var premisesBeforeCreate = getPremisesFromDb(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", String.valueOf(-1)); + params.add("supplier", String.valueOf(1)); + params.add("from_scratch", "false"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isBadRequest()) + .andDo(print()) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesBeforeCreate.size()); + + + } + + @Test + @DisplayName("POST /api/calculation/create - Faulty supplier id") + public void createPremiseCase9() throws Exception { + + var premisesBeforeCreate = getPremisesFromDb(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", String.valueOf(1)); + params.add("supplier", String.valueOf(-1)); + params.add("from_scratch", "false"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isBadRequest()) + .andDo(print()) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesBeforeCreate.size()); + + + } + + @Test + @DisplayName("POST /api/calculation/create - Faulty user supplier id") + public void createPremiseCase10() throws Exception { + var premisesBeforeCreate = getPremisesFromDb(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("material", String.valueOf(1)); + params.add("user_supplier", String.valueOf(-1)); + params.add("from_scratch", "false"); + + var result = mockMvc.perform(post("/api/calculation/create") + .params(params)) + .andExpect(status().isBadRequest()) + .andDo(print()) + .andReturn(); + + var premisesAfterCreate = getPremisesFromDb(); + + assertThat(premisesAfterCreate).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(premisesBeforeCreate.size()); + + } + + private List getPremisesFromDb() { + String query = "SELECT * FROM premise"; + return jdbcTemplate.query(query, new RowMapper<>() { + + @Override + public Premise mapRow(ResultSet rs, int rowNum) throws SQLException { + var entity = new Premise(); + + entity.setId(rs.getInt("id")); + + entity.setPackagingId(rs.getInt("packaging_id")); + if (rs.wasNull()) + entity.setPackagingId(null); + + entity.setMaterialId(rs.getInt("material_id")); + if (rs.wasNull()) + entity.setMaterialId(null); + + entity.setSupplierNodeId(rs.getInt("supplier_node_id")); + if (rs.wasNull()) + entity.setSupplierNodeId(null); + + entity.setUserSupplierNodeId(rs.getInt("user_supplier_node_id")); + if (rs.wasNull()) + entity.setUserSupplierNodeId(null); + + entity.setLocation(new Location(rs.getBigDecimal("geo_lng").doubleValue(), rs.getBigDecimal("geo_lat").doubleValue())); + + entity.setCountryId(rs.getInt("country_id")); + if (rs.wasNull()) + entity.setCountryId(null); + + entity.setUserId(rs.getInt("user_id")); + if (rs.wasNull()) + entity.setUserId(null); + + entity.setMaterialCost(rs.getBigDecimal("material_cost")); + + entity.setHsCode(rs.getString("hs_code")); + + entity.setCustomRate(rs.getBigDecimal("tariff_rate")); + + entity.setFcaEnabled(rs.getBoolean("is_fca_enabled")); + if (rs.wasNull()) + entity.setFcaEnabled(null); + + entity.setOverseaShare(rs.getBigDecimal("oversea_share")); + + entity.setIndividualHuHeight(rs.getInt("individual_hu_height")); + if (rs.wasNull()) + entity.setIndividualHuHeight(null); + + entity.setIndividualHuWidth(rs.getInt("individual_hu_width")); + if (rs.wasNull()) + entity.setIndividualHuWidth(null); + + entity.setIndividualHuLength(rs.getInt("individual_hu_length")); + if (rs.wasNull()) + entity.setIndividualHuLength(null); + + entity.setIndividualHuWeight(rs.getInt("individual_hu_weight")); + if (rs.wasNull()) + entity.setIndividualHuWeight(null); + + entity.setHuDisplayedDimensionUnit(DimensionUnit.valueOf(rs.getString("hu_displayed_dimension_unit"))); + entity.setHuDisplayedWeightUnit(WeightUnit.valueOf(rs.getString("hu_displayed_weight_unit"))); + + entity.setHuStackable(rs.getBoolean("hu_stackable")); + if (rs.wasNull()) + entity.setHuStackable(null); + + entity.setHuMixable(rs.getBoolean("hu_mixable")); + if (rs.wasNull()) + entity.setHuMixable(null); + + entity.setHuUnitCount(rs.getInt("hu_unit_count")); + if (rs.wasNull()) + entity.setHuUnitCount(null); + + entity.setState(PremiseState.valueOf(rs.getString("state"))); + entity.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime()); + entity.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); + + return entity; + } + }); + } + + + private Integer getMaterialIdByPartNumber(String partNumber) { + String query = "SELECT id FROM material WHERE part_number = ? LIMIT 1"; + return jdbcTemplate.queryForObject(query, Integer.class, partNumber); + } + + private List getPremisesWithSupplierId(Integer id) { + String query = "SELECT id FROM premise WHERE supplier_node_id = ?"; + return jdbcTemplate.queryForList(query, Integer.class, id); + } + + private Integer getSupplierIdByName(String name) { + String query = "SELECT id FROM node WHERE name = ? LIMIT 1"; + return jdbcTemplate.queryForObject(query, Integer.class, name); + } + + private Integer getSupplierIdByExternalMappingId(String mappingId) { + String query = "SELECT id FROM node WHERE external_mapping_id = ? LIMIT 1"; + return jdbcTemplate.queryForObject(query, Integer.class, mappingId); + } + + private Integer getUserSupplierIdByName(String name) { + String query = "SELECT id FROM sys_user_node WHERE name = ? LIMIT 1"; + return jdbcTemplate.queryForObject(query, Integer.class, name); + } + } + + @Nested + class SetSupplierTests { + + } + + @Nested + class SetPackagingTests { + + } + + @Nested + class SetPriceTests { + + } + + @Nested + class CreateDestinationTests { + + } + + @Nested + class GetDestinationTests { + + } + + @Nested + class SetDestinationTests { + + } + + @Nested + class DeleteDestinationTests { + + } + +// @Nested +// class claudeTests { +// +// // Test for GET /api/calculation/view +// @Test +// @Sql(scripts = {"classpath:master_data/alldata.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:master_data/alldata-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testListPremises_WithValidFilter_ReturnsFilteredResults() throws Exception { +// mockMvc.perform(get("/api/calculation/view") +// .param("filter", "test") +// .param("page", "1") +// .param("limit", "10") +// .param("deleted", "false") +// .param("archived", "false") +// .param("done", "true")) +// .andExpect(status().isOk()) +// .andExpect(header().exists("X-Total-Count")) +// .andExpect(header().exists("X-Page-Count")) +// .andExpect(header().string("X-Current-Page", "1")) +// .andExpect(jsonPath("$", isA(List.class))) +// .andExpect(jsonPath("$[0].id").exists()) +// .andExpect(jsonPath("$[0].material").exists()) +// .andExpect(jsonPath("$[0].supplier").exists()) +// .andExpect(jsonPath("$[0][?(@.state in ['draft', 'completed', 'archived', 'deleted'])]").exists()); +// } +// +// @Test +// public void testListPremises_WithMinimalParameters_ReturnsResults() throws Exception { +// mockMvc.perform(get("/api/calculation/view") +// .param("filter", "")) +// .andExpect(status().isOk()) +// .andExpect(header().exists("X-Total-Count")) +// .andExpect(jsonPath("$", isA(List.class))); +// } +// +// @Test +// public void testListPremises_WithInvalidPageParameter_ReturnsBadRequest() throws Exception { +// mockMvc.perform(get("/api/calculation/view") +// .param("filter", "test") +// .param("page", "0")) // Min value is 1 +// .andExpect(status().isBadRequest()); +// } +// +// // Test for GET /api/calculation/search +// @Test +// @Sql(scripts = {"classpath:test-data/material-supplier-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/material-supplier-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testSearchMaterialsAndSuppliers_WithValidSearch_ReturnsResults() throws Exception { +// mockMvc.perform(get("/api/calculation/search") +// .param("search", "test material")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$.materials", isA(List.class))) +// .andExpect(jsonPath("$.supplier", isA(List.class))) +// .andExpect(jsonPath("$.user_supplier", isA(List.class))); +// } +// +// @Test +// public void testSearchMaterialsAndSuppliers_WithEmptySearch_ReturnsEmptyResults() throws Exception { +// mockMvc.perform(get("/api/calculation/search") +// .param("search", "")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$.materials", empty())) +// .andExpect(jsonPath("$.supplier", empty())) +// .andExpect(jsonPath("$.user_supplier", empty())); +// } +// +// // Test for POST /api/calculation/create +// @Test +// @Sql(scripts = {"classpath:test-data/create-premise-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/create-premise-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testCreatePremises_WithValidData_ReturnsCreatedPremises() throws Exception { +// mockMvc.perform(post("/api/calculation/create") +// .param("material", "1", "2") +// .param("supplier", "1", "2") +// .param("user_supplier", "1") +// .param("from_scratch", "false")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$", isA(List.class))) +// .andExpect(jsonPath("$[0].id").exists()) +// .andExpect(jsonPath("$[0].material").exists()) +// .andExpect(jsonPath("$[0].supplier").exists()); +// } +// +// @Test +// public void testCreatePremises_FromScratch_ReturnsEmptyPremises() throws Exception { +// mockMvc.perform(post("/api/calculation/create") +// .param("material", "") +// .param("supplier", "") +// .param("user_supplier", "") +// .param("from_scratch", "true")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$", isA(List.class))); +// } +// +// // Test for GET /api/calculation/edit +// @Test +// @Sql(scripts = {"classpath:test-data/premise-detail-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/premise-detail-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testGetPremises_WithValidIds_ReturnsDetailedPremises() throws Exception { +// mockMvc.perform(get("/api/calculation/edit") +// .param("premiss_ids", "1", "2")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$", hasSize(2))) +// .andExpect(jsonPath("$[0].id").exists()) +// .andExpect(jsonPath("$[0].material.hs_code").exists()) +// .andExpect(jsonPath("$[0].supplier.location").exists()) +// .andExpect(jsonPath("$[0].destinations").exists()) +// .andExpect(jsonPath("$[0].hu").exists()); +// } +// +// @Test +// public void testGetPremises_WithNonExistentIds_ReturnsEmptyList() throws Exception { +// mockMvc.perform(get("/api/calculation/edit") +// .param("premiss_ids", "999999")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$", empty())); +// } +// +// // Test for PUT /api/calculation/start +// @Test +// @Sql(scripts = {"classpath:test-data/calculation-start-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/calculation-start-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testStartCalculation_WithValidIds_ReturnsProcessingId() throws Exception { +// List premiseIds = Arrays.asList(1, 2, 3); +// +// mockMvc.perform(put("/api/calculation/start") +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(premiseIds))) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$").isNumber()); +// } +// +// // Test for GET /api/calculation/status/{processing_id} +// @Test +// @Sql(scripts = {"classpath:test-data/calculation-status-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/calculation-status-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testGetCalculationStatus_WithValidId_ReturnsStatus() throws Exception { +// mockMvc.perform(get("/api/calculation/status/1")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$.status").exists()) +// .andExpect(jsonPath("$.progress").exists()); +// } +// +// @Test +// public void testGetCalculationStatus_WithInvalidId_ReturnsNotFound() throws Exception { +// mockMvc.perform(get("/api/calculation/status/999999")) +// .andExpect(status().isNotFound()); +// } +// +// // Test for PUT /api/calculation/packaging +// @Test +// @Sql(scripts = {"classpath:test-data/packaging-update-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/packaging-update-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testUpdatePackaging_WithValidData_ReturnsSuccess() throws Exception { +// PackagingUpdateDTO packagingDTO = new PackagingUpdateDTO(); +// +// DimensionDTO dimension = new DimensionDTO(); +// dimension.setLength(100.0); +// dimension.setWidth(80.0); +// dimension.setHeight(60.0); +// dimension.setDimensionUnit(DimensionUnit.CM); +// dimension.setWeight(10.0); +// dimension.setWeightUnit(WeightUnit.KG); +// dimension.setContentUnitCount(20); +// +// packagingDTO.setDimensions(dimension); +// packagingDTO.setPremiseIds(Arrays.asList(1, 2)); +// packagingDTO.setMixable(true); +// packagingDTO.setMixable(true); +// +// mockMvc.perform(put("/api/calculation/packaging") +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(packagingDTO))) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$").isMap()); +// } +// +// // Test for POST /api/calculation/material (update) +// @Test +// @Sql(scripts = {"classpath:test-data/material-update-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/material-update-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testUpdateMaterial_WithValidData_ReturnsSuccess() throws Exception { +// MaterialUpdateDTO materialUpdateDTO = new MaterialUpdateDTO(); +// materialUpdateDTO.setPremiseIds(Arrays.asList(1, 2)); +// +// materialUpdateDTO.setHsCode("12345678"); +// materialUpdateDTO.setTariffRate(new BigDecimal("0.05")); +// +// +// mockMvc.perform(post("/api/calculation/material") +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(materialUpdateDTO))) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$").isMap()); +// } +// +// // Test for PUT /api/calculation/price +// @Test +// @Sql(scripts = {"classpath:test-data/price-update-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/price-update-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testUpdatePrice_WithValidData_ReturnsSuccess() throws Exception { +// PriceUpdateDTO priceUpdateDTO = new PriceUpdateDTO(); +// priceUpdateDTO.setPremiseIds(Arrays.asList(1, 2)); +// +// +// priceUpdateDTO.setPrice(new BigDecimal("100.50")); +// priceUpdateDTO.setOverseaShare(new BigDecimal("0.25")); +// priceUpdateDTO.setIncludeFcaFee(true); +// +// +// mockMvc.perform(put("/api/calculation/price") +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(priceUpdateDTO))) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$").isMap()); +// } +// +// // Test for POST /api/calculation/destination +// @Test +// @Sql(scripts = {"classpath:test-data/destination-create-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/destination-create-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testCreateDestination_WithValidData_ReturnsCreatedDestinations() throws Exception { +// DestinationCreateDTO destinationCreateDTO = new DestinationCreateDTO(); +// destinationCreateDTO.setPremiseId(Arrays.asList(1, 2)); +// destinationCreateDTO.setDestinationNodeId(10); +// +// mockMvc.perform(post("/api/calculation/destination") +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(destinationCreateDTO))) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$").isMap()) +// .andExpect(jsonPath("$.1").exists()) +// .andExpect(jsonPath("$.1.destination_node").exists()) +// .andExpect(jsonPath("$.1.routes").isArray()); +// } +// +// // Test for GET /api/calculation/destination/{id} +// @Test +// @Sql(scripts = {"classpath:test-data/destination-get-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/destination-get-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testGetDestination_WithValidId_ReturnsDestination() throws Exception { +// mockMvc.perform(get("/api/calculation/destination/1")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$.id").value("1")) +// .andExpect(jsonPath("$.country").exists()) +// .andExpect(jsonPath("$.name").exists()) +// .andExpect(jsonPath("$.routes").isArray()); +// } +// +// @Test +// public void testGetDestination_WithInvalidId_ReturnsNotFound() throws Exception { +// mockMvc.perform(get("/api/calculation/destination/999999")) +// .andExpect(status().isNotFound()); +// } +// +// // Test for PUT /api/calculation/destination/{id} +// @Test +// @Sql(scripts = {"classpath:test-data/destination-update-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/destination-update-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testUpdateDestination_WithValidData_ReturnsSuccess() throws Exception { +// DestinationUpdateDTO updateDTO = new DestinationUpdateDTO(); +// updateDTO.setRepackingCost(new BigDecimal("50.00")); +// updateDTO.setHandlingCost(new BigDecimal("25.00")); +// updateDTO.setDisposalCost(new BigDecimal("10.00")); +// updateDTO.setRouteSelectedId(1); +// +// mockMvc.perform(put("/api/calculation/destination/1") +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(updateDTO))) +// .andExpect(status().isOk()); +// } +// +// // Test for DELETE /api/calculation/destination/{id} +// @Test +// @Sql(scripts = {"classpath:test-data/destination-delete-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/destination-delete-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testDeleteDestination_WithValidId_ReturnsSuccess() throws Exception { +// mockMvc.perform(delete("/api/calculation/destination/1")) +// .andExpect(status().isOk()); +// } +// +// @Test +// public void testDeleteDestination_WithInvalidId_ReturnsNotFound() throws Exception { +// mockMvc.perform(delete("/api/calculation/destination/999999")) +// .andExpect(status().isNotFound()); +// } +// +// // Test for PUT /api/calculation/supplier +// @Test +// @Sql(scripts = {"classpath:test-data/supplier-set-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/supplier-set-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testSetSupplier_WithValidData_ReturnsUpdatedPremises() throws Exception { +// SetDataDTO setSupplierDTO = new SetDataDTO(); +// setSupplierDTO.setPremiseId(Arrays.asList(1, 2)); +// setSupplierDTO.setUpdateMasterData(true); +// setSupplierDTO.setSupplierNodeId(5); +// +// mockMvc.perform(put("/api/calculation/supplier") +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(setSupplierDTO))) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$").isArray()) +// .andExpect(jsonPath("$[0].supplier.id").value("5")); +// } +// +// // Test for PUT /api/calculation/material (set) +// @Test +// @Sql(scripts = {"classpath:test-data/material-set-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/material-set-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testSetMaterial_WithValidData_ReturnsUpdatedPremises() throws Exception { +// SetDataDTO setMaterialDTO = new SetDataDTO(); +// setMaterialDTO.setPremiseId(Arrays.asList(1, 2)); +// setMaterialDTO.setUpdateMasterData(false); +// setMaterialDTO.setMaterialId(3); +// +// mockMvc.perform(put("/api/calculation/material") +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString(setMaterialDTO))) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$").isArray()) +// .andExpect(jsonPath("$[0].material.id").value("3")); +// } +// +// // Edge Cases and Error Handling Tests +// @Test +// public void testEndpoint_WithMissingRequiredParameter_ReturnsBadRequest() throws Exception { +// mockMvc.perform(get("/api/calculation/view")) +// // Missing required 'filter' parameter +// .andExpect(status().isBadRequest()); +// } +// +// @Test +// public void testEndpoint_WithInvalidJSON_ReturnsBadRequest() throws Exception { +// mockMvc.perform(put("/api/calculation/start") +// .contentType(MediaType.APPLICATION_JSON) +// .content("invalid json")) +// .andExpect(status().isBadRequest()); +// } +// +// @Test +// public void testEndpoint_WithEmptyList_ReturnsAppropriateResponse() throws Exception { +// mockMvc.perform(put("/api/calculation/start") +// .contentType(MediaType.APPLICATION_JSON) +// .content("[]")) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$").value(0)); +// } +// +// // Performance Test (simplified) +// @Test +// @Sql(scripts = {"classpath:test-data/performance-test-data.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +// @Sql(scripts = {"classpath:test-data/performance-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +// public void testListPremises_WithLargeDataset_PerformsWithinAcceptableTime() throws Exception { +// long startTime = System.currentTimeMillis(); +// +// mockMvc.perform(get("/api/calculation/view") +// .param("filter", "") +// .param("page", "1") +// .param("limit", "100")) +// .andExpect(status().isOk()); +// +// long endTime = System.currentTimeMillis(); +// long duration = endTime - startTime; +// +// // Assert that the request completes within 2 seconds +// assert duration < 2000 : "Request took too long: " + duration + "ms"; +// } +//} +} \ No newline at end of file diff --git a/src/test/java/de/avatic/lcc/controller/calculation/PremiseControllerTestData.java b/src/test/java/de/avatic/lcc/controller/calculation/PremiseControllerTestData.java new file mode 100644 index 0000000..017408d --- /dev/null +++ b/src/test/java/de/avatic/lcc/controller/calculation/PremiseControllerTestData.java @@ -0,0 +1,318 @@ +package de.avatic.lcc.controller.calculation; + +import de.avatic.lcc.dto.generic.RateType; +import de.avatic.lcc.dto.generic.TransportType; +import de.avatic.lcc.model.nodes.Node; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.sql.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@TestConfiguration +public class PremiseControllerTestData { + + private final JdbcTemplate jdbcTemplate; + + public PremiseControllerTestData(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Transactional + public void deleteAll() { + jdbcTemplate.execute("DELETE FROM premise_route_section"); + jdbcTemplate.execute("DELETE FROM premise_route_node"); + jdbcTemplate.execute("DELETE FROM premise_route"); + jdbcTemplate.execute("DELETE FROM premise_destination"); + + jdbcTemplate.execute("ALTER TABLE premise_route_section AUTO_INCREMENT = 1;"); + jdbcTemplate.execute("ALTER TABLE premise_route_node AUTO_INCREMENT = 1;"); + jdbcTemplate.execute("ALTER TABLE premise_route AUTO_INCREMENT = 1;"); + jdbcTemplate.execute("ALTER TABLE premise_destination AUTO_INCREMENT = 1;"); + } + + + @Transactional + public void generateData() { + + var countryIdDE = getCountryId("DE"); + var countryIdCZ = getCountryId("CZ"); + var countryIdFR = getCountryId("FR"); + + Node nodeAb = getNode("AB"); + Node nodeStr = getNode("STR"); + Node nodeCtt = getNode("CTT"); + + Node nodeLx = getNode("LX"); + Node nodeCnxmn = getNode("CNXMN"); + Node nodeDeham = getNode("DEHAM"); + Node nodeBezee = getNode("BEZEE"); + + var destinationId1 = createPremiseDestination(getPremise1Id(), 110, nodeAb.getId(), false, null, null, BigDecimal.valueOf(10), BigDecimal.valueOf(10), BigDecimal.valueOf(10), BigDecimal.valueOf(49.9506), BigDecimal.valueOf(9.1038), countryIdDE); + var destinationId2 = createPremiseDestination(getPremise1Id(), 120, nodeStr.getId(), false, null, null, BigDecimal.valueOf(12), BigDecimal.valueOf(12), BigDecimal.valueOf(12), BigDecimal.valueOf(49.7002), BigDecimal.valueOf(13.035), countryIdCZ); + + var routeId1 = createPremiseRoute(destinationId1, false, false, true); + var routeId2 = createPremiseRoute(destinationId1, true, true, false); + + var route1NodeId1 = createPremiseRouteNode(nodeLx, false, false, false, true, false); + var route1NodeId2 = createPremiseRouteNode(nodeCnxmn, false, false, true, false, false); + var route1NodeId3 = createPremiseRouteNode(nodeDeham, false, false, true, false, false); + var route1NodeId4 = createPremiseRouteNode(nodeAb, false, true, false, false, false); + + var route1section1 = createPremiseRouteSection(routeId1, route1NodeId1, route1NodeId2, 1, TransportType.ROAD, RateType.CONTAINER, true, false, false, false); + var route1section2 = createPremiseRouteSection(routeId1, route1NodeId2, route1NodeId3, 2, TransportType.SEA, RateType.CONTAINER, false, true, false, false); + var route1section3 = createPremiseRouteSection(routeId1, route1NodeId3, route1NodeId4, 3, TransportType.POST_RUN, RateType.CONTAINER, false, false, true, false); + + var route2NodeId1 = createPremiseRouteNode(nodeLx, false, false, false, true, false); + var route2NodeId2 = createPremiseRouteNode(nodeCnxmn, false, false, true, false, false); + var route2NodeId3 = createPremiseRouteNode(nodeBezee, false, false, true, false, false); + var route2NodeId4 = createPremiseRouteNode(nodeAb, false, true, false, false, false); + + var route2section1 = createPremiseRouteSection(routeId2, route2NodeId1, route2NodeId2, 1, TransportType.ROAD, RateType.CONTAINER, true, false, false, false); + var route2section2 = createPremiseRouteSection(routeId2, route2NodeId2, route2NodeId3, 2, TransportType.SEA, RateType.CONTAINER, false, true, false, false); + var route2section3 = createPremiseRouteSection(routeId2, route2NodeId3, route2NodeId4, 3, TransportType.POST_RUN, RateType.CONTAINER, false, false, true, false); + + var destinationId3 = createPremiseDestination(getPremise3Id(), 310, nodeCtt.getId(), true, BigDecimal.valueOf(3000.0), 30, BigDecimal.valueOf(30), BigDecimal.valueOf(30),BigDecimal.valueOf(30),BigDecimal.valueOf(46.7762),BigDecimal.valueOf(0.5351), countryIdFR); + + + + } + + private Integer createPremiseRouteSection(Integer routeId, Integer fromNode, Integer toNode, Integer listPosition, TransportType type, RateType rateType, boolean isPreRun, boolean isMainRun, boolean isPostRun, boolean isOutdated) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + + String query = """ + INSERT INTO premise_route_section ( premise_route_id, from_route_node_id, to_route_node_id, list_position, transport_type, rate_type, is_pre_run, is_main_run, is_post_run, is_outdated) + VALUES (?, ?, ?, ?, ?, ?, ? ,?, ?, ?); + """; + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + + ps.setInt(1, routeId); + ps.setInt(2, fromNode); + ps.setInt(3, toNode); + ps.setInt(4, listPosition); + ps.setString(5, type.name()); + ps.setString(6, rateType.name()); + ps.setBoolean(7, isPreRun); + ps.setBoolean(8, isMainRun); + ps.setBoolean(9, isPostRun); + ps.setBoolean(10, isOutdated); + + return ps; + }, keyHolder); + + return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null; + + } + + private Integer createPremiseRouteNode(Node node, Boolean isUserNode, Boolean isDestination, Boolean isIntermediate, Boolean isSource, Boolean isOutdated) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + + String query = """ + INSERT INTO premise_route_node ( node_id, user_node_id, name, address, country_id, is_destination, is_intermediate, is_source, geo_lat, geo_lng, is_outdated) + VALUES (?, ?, ?, ?, ?, ?, ? ,?, ?, ?, ?); + """; + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + + if (isUserNode) { + ps.setNull(1, Types.INTEGER); + } else { + ps.setInt(1, node.getId()); + } + + if (!isUserNode) { + ps.setNull(2, Types.INTEGER); + } else { + ps.setInt(2, node.getId()); + } + + ps.setString(3, node.getName()); + ps.setString(4, node.getAddress()); + ps.setInt(5, node.getCountryId()); + ps.setBoolean(6, isDestination); + ps.setBoolean(7, isIntermediate); + ps.setBoolean(8, isSource); + ps.setBigDecimal(9, node.getGeoLat()); + ps.setBigDecimal(10, node.getGeoLng()); + ps.setBoolean(11, isOutdated); + + return ps; + }, keyHolder); + + return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null; + } + + private Node getNode(String externalMappingId) { + String query = "SELECT * FROM node WHERE external_mapping_id = ?"; + return jdbcTemplate.queryForObject(query, new NodeMapper(), externalMappingId); + } + + private Integer getPremise1Id() { + String query = "SELECT id FROM premise WHERE material_id = (SELECT id FROM material WHERE part_number = '28152640129') AND hu_unit_count = 2"; + + + return jdbcTemplate.queryForObject(query, Integer.class); + } + + + private Integer getPremise3Id() { + String query = "SELECT id FROM premise WHERE material_id = (SELECT id FROM material WHERE part_number = '8222640822') AND hu_unit_count = 3"; + + + + return jdbcTemplate.queryForObject(query, Integer.class); + } + + private Integer getCountryId(String isoCode) { + String query = "SELECT id FROM country WHERE iso_code = ?"; + return jdbcTemplate.queryForObject(query, Integer.class, isoCode); + } + + private Integer getNodeId(String externalMappingId) { + String query = "SELECT id FROM node WHERE external_mapping_id = ?"; + return jdbcTemplate.queryForObject(query, Integer.class, externalMappingId); + } + + private Integer createPremiseRoute(Integer premiseDestinationId, Boolean isFastest, Boolean isCheapest, Boolean isSelected) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + + String query = """ + INSERT INTO premise_route ( premise_destination_id, is_fastest, is_cheapest, is_selected) + VALUES (?, ?, ?, ?); + """; + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + + ps.setInt(1, premiseDestinationId); + ps.setBoolean(2, isFastest); + ps.setBoolean(3, isCheapest); + ps.setBoolean(4, isSelected); + + return ps; + }, keyHolder); + + return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null; + } + + private Integer createPremiseDestination(Integer premiseId, Integer annualAmount, Integer destinationNodeId, + Boolean isD2D, BigDecimal rateD2D, Integer leadTimeD2D, BigDecimal repackingCost, BigDecimal handlingCost, + BigDecimal disposalCost, BigDecimal geoLat, BigDecimal geoLng, Integer countryId) { + + KeyHolder keyHolder = new GeneratedKeyHolder(); + + String query = """ + INSERT INTO premise_destination (premise_id, + annual_amount, + destination_node_id, + is_d2d, + rate_d2d, + lead_time_d2d, + repacking_cost, + handling_cost, + disposal_cost, + geo_lat, + geo_lng, + country_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + """; + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + + ps.setInt(1, premiseId); + ps.setInt(2, annualAmount); + ps.setInt(3, destinationNodeId); + ps.setBoolean(4, isD2D); + ps.setBigDecimal(5, rateD2D); + if (leadTimeD2D == null) ps.setNull(6, Types.INTEGER); + else ps.setInt(6, leadTimeD2D); + ps.setBigDecimal(7, repackingCost); + ps.setBigDecimal(8, handlingCost); + ps.setBigDecimal(9, disposalCost); + ps.setBigDecimal(10, geoLat); + ps.setBigDecimal(11, geoLng); + ps.setInt(12, countryId); + + return ps; + }, keyHolder); + + return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null; + } + + private List> getPredecessorsOf(Integer id) { + + String queryChains = """ + SELECT chain.id AS id FROM node_predecessor_chain AS chain WHERE chain.node_id = ? ORDER BY chain.id + """; + + return jdbcTemplate.query(queryChains, (chainRs, rowNum) -> { + + Integer chainId = chainRs.getInt("id"); + + String query = """ + SELECT entry.node_id AS predecessor_node_id , entry.sequence_number as sequence_number + FROM node_predecessor_entry AS entry + WHERE entry.node_predecessor_chain_id = ? ORDER BY entry.sequence_number"""; + + return jdbcTemplate.query(query, rs -> { + Map predecessors = new HashMap<>(); + + while (rs.next()) { + predecessors.put(rs.getInt("sequence_number"), rs.getInt("predecessor_node_id")); + } + + return predecessors; + }, chainId); + }, id); + } + + private Collection getOutboundCountriesOf(Integer id) { + String query = """ + SELECT outbound_country_mapping.country_id + FROM outbound_country_mapping + WHERE outbound_country_mapping.node_id = ?"""; + + return jdbcTemplate.queryForList(query, Integer.class, id); + } + + private class NodeMapper implements RowMapper { + + @Override + public Node mapRow(ResultSet rs, int rowNum) throws SQLException { + var data = new Node(); + + data.setId(rs.getInt("id")); + data.setName(rs.getString("name")); + data.setAddress(rs.getString("address")); + data.setCountryId(rs.getInt("country_id")); + data.setDestination(rs.getBoolean("is_destination")); + data.setSource(rs.getBoolean("is_source")); + data.setIntermediate(rs.getBoolean("is_intermediate")); + data.setPredecessorRequired(rs.getBoolean("predecessor_required")); + + data.setGeoLat(rs.getBigDecimal("geo_lat")); + data.setGeoLng(rs.getBigDecimal("geo_lng")); + + data.setDeprecated(rs.getBoolean("is_deprecated")); + + data.setNodePredecessors(getPredecessorsOf(data.getId())); + data.setOutboundCountries(getOutboundCountriesOf(data.getId())); + + return data; + } + } + +} diff --git a/src/test/java/de/avatic/lcc/controller/configuration/NodeControllerIntegrationTest.java b/src/test/java/de/avatic/lcc/controller/configuration/NodeControllerIntegrationTest.java new file mode 100644 index 0000000..1ccd074 --- /dev/null +++ b/src/test/java/de/avatic/lcc/controller/configuration/NodeControllerIntegrationTest.java @@ -0,0 +1,450 @@ +package de.avatic.lcc.controller.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.avatic.lcc.dto.configuration.nodes.update.NodeUpdateDTO; +import de.avatic.lcc.dto.configuration.nodes.userNodes.AddUserNodeDTO; +import de.avatic.lcc.dto.generic.CountryDTO; +import de.avatic.lcc.dto.generic.LocationDTO; +import de.avatic.lcc.dto.generic.NodeType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class NodeControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Nested + @DisplayName("GET /api/nodes/ - List Nodes") + class ListNodesTests { + + @Test + @DisplayName("Should return list of nodes with default pagination") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldReturnListOfNodesWithDefaultPagination() throws Exception { + mockMvc.perform(get("/api/nodes/")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(header().exists("X-Total-Count")) + .andExpect(header().exists("X-Page-Count")) + .andExpect(header().exists("X-Current-Page")) + .andExpect(jsonPath("$", isA(java.util.List.class))) + .andExpect(jsonPath("$", hasSize(greaterThan(0)))) + .andExpect(jsonPath("$[0].id", notNullValue())) + .andExpect(jsonPath("$[0].country", notNullValue())) + .andExpect(jsonPath("$[0].country.id", notNullValue())) + .andExpect(jsonPath("$[0].country.iso_code", notNullValue())) + .andExpect(jsonPath("$[0].country.region_code", notNullValue())) + .andExpect(jsonPath("$[0].address", notNullValue())) + .andExpect(jsonPath("$[0].location", notNullValue())) + .andExpect(jsonPath("$[0].location.longitude", notNullValue())) + .andExpect(jsonPath("$[0].location.latitude", notNullValue())) + .andExpect(jsonPath("$[0].types", isA(java.util.List.class))); + } + + @Test + @DisplayName("Should return filtered nodes when filter parameter is provided") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldReturnFilteredNodes() throws Exception { + mockMvc.perform(get("/api/nodes/") + .param("filter", "Shanghai")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", isA(java.util.List.class))) + .andExpect(jsonPath("$[*].name", everyItem(containsStringIgnoringCase("Shanghai")))); + } + + @Test + @DisplayName("Should respect pagination parameters") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldRespectPaginationParameters() throws Exception { + mockMvc.perform(get("/api/nodes/") + .param("page", "1") + .param("limit", "5")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(header().string("X-Current-Page", "1")) + .andExpect(jsonPath("$", hasSize(lessThanOrEqualTo(5)))); + } + + @Test + @DisplayName("Should return empty list when no nodes match filter") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldReturnEmptyListWhenNoNodesMatchFilter() throws Exception { + mockMvc.perform(get("/api/nodes/") + .param("filter", "NonExistentCity")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(0))) + .andExpect(header().string("X-Total-Count", "0")); + } + } + + @Nested + @DisplayName("GET /api/nodes/search - Search Nodes") + class SearchNodesTests { + + @Test + @DisplayName("Should search nodes without type filter") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldSearchNodesWithoutTypeFilter() throws Exception { + mockMvc.perform(get("/api/nodes/search") + .param("filter", "KION") + .param("limit", "10")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", isA(java.util.List.class))) + .andExpect(jsonPath("$", hasSize(lessThanOrEqualTo(10)))); + } + + @Test + @DisplayName("Should search nodes with specific node type") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldSearchNodesWithSpecificNodeType() throws Exception { + mockMvc.perform(get("/api/nodes/search") + .param("node_type", "SOURCE") + .param("limit", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", isA(java.util.List.class))) + .andExpect(jsonPath("$[*].types", everyItem(hasItem("source")))); + } + + @Test + @DisplayName("Should include user nodes when requested") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldIncludeUserNodesWhenRequested() throws Exception { + mockMvc.perform(get("/api/nodes/search") + .param("include_user_node", "true") + .param("limit", "20")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$", isA(java.util.List.class))); + } + } + + @Nested + @DisplayName("GET /api/nodes/{id} - Get Node Details") + class GetNodeDetailsTests { + + @Test + @DisplayName("Should return node details for existing node") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldReturnNodeDetailsForExistingNode() throws Exception { + // Using node ID 16 (LX - Linde China) from test data + mockMvc.perform(get("/api/nodes/16")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(jsonPath("$.id", is(16))) + .andExpect(jsonPath("$.country", notNullValue())) + .andExpect(jsonPath("$.country.iso_code", is("CN"))) + .andExpect(jsonPath("$.name", notNullValue())) + .andExpect(jsonPath("$.address", notNullValue())) + .andExpect(jsonPath("$.location", notNullValue())) + .andExpect(jsonPath("$.location.longitude", notNullValue())) + .andExpect(jsonPath("$.location.latitude", notNullValue())) + .andExpect(jsonPath("$.types", isA(java.util.List.class))) + .andExpect(jsonPath("$.types", hasItem("source"))) + .andExpect(jsonPath("$.predecessors", notNullValue())) + .andExpect(jsonPath("$.outbound_countries", isA(java.util.List.class))) + .andExpect(jsonPath("$.is_deprecated", is(false))); + } + + @Test + @DisplayName("Should return bad request for non-existing node") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldReturn400ForNonExistingNode() throws Exception { + mockMvc.perform(get("/api/nodes/99999")).andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Should return node with predecessor chains") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldReturnNodeWithPredecessorChains() throws Exception { + // Using node ID 24 (CTT - Châtellerault) which has predecessor chains + mockMvc.perform(get("/api/nodes/24")) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(jsonPath("$.id", is(24))) + .andExpect(jsonPath("$.predecessors", notNullValue())) + .andExpect(jsonPath("$.predecessors", not(empty()))); + } + } + + @Nested + @DisplayName("PUT /api/nodes/{id} - Update Node") + class UpdateNodeTests { + + @Test + @DisplayName("Should update existing node successfully") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldUpdateExistingNodeSuccessfully() throws Exception { + NodeUpdateDTO updateDTO = new NodeUpdateDTO(); + updateDTO.setId(16); + updateDTO.setAddress("Updated Address for Linde China"); + // Set other required fields based on your DTO structure + + mockMvc.perform(put("/api/nodes/16") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateDTO))) + .andExpect(status().isOk()) + .andExpect(content().string("16")); + + // Verify the update + mockMvc.perform(get("/api/nodes/16")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.address", is("Updated Address for Linde China"))); + } + + @Test + @DisplayName("Should return 400 when ID in path doesn't match ID in body") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql" + }, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldReturn400WhenIdMismatch() throws Exception { + NodeUpdateDTO updateDTO = new NodeUpdateDTO(); + updateDTO.setId(17); // Different from path parameter + updateDTO.setAddress("Some address"); + + mockMvc.perform(put("/api/nodes/16") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateDTO))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Should return 400 when trying to update non-existing node") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldReturn400WhenUpdatingNonExistingNode() throws Exception { + NodeUpdateDTO updateDTO = new NodeUpdateDTO(); + updateDTO.setId(99999); + updateDTO.setAddress("Some address"); + + mockMvc.perform(put("/api/nodes/99999") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateDTO))) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("DELETE /api/nodes/{id} - Delete Node") + class DeleteNodeTests { + + @Test + @DisplayName("Should mark node as deprecated successfully") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldMarkNodeAsDeprecatedSuccessfully() throws Exception { + mockMvc.perform(delete("/api/nodes/16")) + .andExpect(status().isOk()) + .andExpect(content().string("16")); + + // Verify the node is no longer in the regular list (since deprecated nodes are filtered out) + mockMvc.perform(get("/api/nodes/")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[?(@.id == '16')]", hasSize(0))); + } + + @Test + @DisplayName("Should return 404 when trying to delete non-existing node") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldReturn404WhenDeletingNonExistingNode() throws Exception { + mockMvc.perform(delete("/api/nodes/99999")) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("GET /api/nodes/locate - Locate Node") + class LocateNodeTests { + + @Test + @DisplayName("Should locate node by address") + void shouldLocateNodeByAddress() throws Exception { + mockMvc.perform(get("/api/nodes/locate") + .param("address", "Berlin, Germany")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", notNullValue())); + // Note: This test depends on the GeoApiService implementation + // You might want to mock the service for more predictable results + } + + @Test + @DisplayName("Should return 400 when address parameter is missing") + void shouldReturn400WhenAddressParameterIsMissing() throws Exception { + mockMvc.perform(get("/api/nodes/locate")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Should handle invalid addresses gracefully") + void shouldHandleInvalidAddressesGracefully() throws Exception { + mockMvc.perform(get("/api/nodes/locate") + .param("address", "InvalidAddressThatDoesNotExist123")) + .andExpect(status().isOk()); // Depends on GeoApiService implementation + // The actual behavior depends on how your GeoApiService handles invalid addresses + } + } + + @Nested + @DisplayName("PUT /api/nodes/ - Add User Node") + class AddUserNodeTests { + + @Test + @DisplayName("Should add user node successfully") + @Sql(scripts = {"classpath:master_data/countries_properties.sql", "classpath:master_data/nodes.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"classpath:master_data/nodes-cleanup.sql", "classpath:master_data/countries_properties-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + void shouldAddUserNodeSuccessfully() throws Exception { + + CountryDTO countryDTO = new CountryDTO(); + countryDTO.setIsoCode("DE"); + + LocationDTO locationDTO = new LocationDTO(52.5139,13.4079); + + AddUserNodeDTO addUserNodeDTO = new AddUserNodeDTO(); + addUserNodeDTO.setName("MyUserNode"); + addUserNodeDTO.setAddress("Rathausstraße 5, 10178 Berlin"); + addUserNodeDTO.setCountry(countryDTO); + addUserNodeDTO.setLocation(locationDTO); + + + mockMvc.perform(put("/api/nodes/") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(addUserNodeDTO))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("Should return 400 for invalid user node data") + void shouldReturn400ForInvalidUserNodeData() throws Exception { + AddUserNodeDTO invalidDTO = new AddUserNodeDTO(); + // Leave required fields empty to trigger validation errors + + mockMvc.perform(put("/api/nodes/") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidDTO))) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("Error Handling Tests") + class ErrorHandlingTests { + + @Test + @DisplayName("Should handle malformed JSON in request body") + void shouldHandleMalformedJsonInRequestBody() throws Exception { + String malformedJson = "{ \"id\": \"invalid\" malformed }"; + + mockMvc.perform(put("/api/nodes/16") + .contentType(MediaType.APPLICATION_JSON) + .content(malformedJson)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Should handle non-numeric node ID in path") + void shouldHandleNonNumericNodeIdInPath() throws Exception { + mockMvc.perform(get("/api/nodes/invalid-id")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Should handle negative pagination parameters") + void shouldHandleNegativePaginationParameters() throws Exception { + mockMvc.perform(get("/api/nodes/") + .param("page", "-1") + .param("limit", "-5")) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("Security Tests") + class SecurityTests { + + @Test + @DisplayName("Should reject requests with XSS attempts in filter") + void shouldRejectRequestsWithXssAttemptsInFilter() throws Exception { + String xssAttempt = ""; + + mockMvc.perform(get("/api/nodes/") + .param("filter", xssAttempt)) + .andExpect(status().isOk()) // Should not fail but should sanitize + .andExpect(jsonPath("$[*].address", not(containsString("