Added Premise Controller Integration Tests

This commit is contained in:
Jan 2025-07-19 22:10:24 +02:00
parent 438fabefb9
commit a9275a012a
67 changed files with 7747 additions and 2727 deletions

View file

@ -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<ErrorResponseDTO> 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<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
public ResponseEntity<ErrorResponseDTO> 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<ErrorResponseDTO> 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)

View file

@ -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<List<PremiseDTO>> 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<List<PremiseDTO>> 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<PremiseSearchResultDTO> findMaterialsAndSuppliers(@RequestParam String search) {
return ResponseEntity.ok(premiseSearchStringAnalyzerService.findMaterialAndSuppliers(search));
}
@PostMapping("/create")
public ResponseEntity<List<PremiseDetailDTO>> createPremises(@RequestParam("material") List<Integer> materialIds, @RequestParam("supplier") List<Integer> supplierIds, @RequestParam("user_supplier") List<Integer> userSupplierIds, @RequestParam("from_scratch") boolean createEmpty) {
return ResponseEntity.ok(premiseCreationService.createPremises(materialIds, supplierIds, userSupplierIds, createEmpty));
@PostMapping({"/create", "/create/"})
public ResponseEntity<List<PremiseDetailDTO>> createPremises(@RequestParam("material") List<String> materialIds,
@RequestParam(value = "supplier", required = false) List<String> supplierIds,
@RequestParam(name = "user_supplier", required = false) List<String> userSupplierIds,
@RequestParam(name = "from_scratch", defaultValue = "true") boolean createEmpty) {
List<Integer> suppliers;
List<Integer> userSuppliers;
List<Integer> 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 + ".");
}
@GetMapping("/edit")
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", "/edit/"})
public ResponseEntity<List<PremiseDetailDTO>> getPremises(@RequestParam("premiss_ids") List<Integer> premissIds) {
return ResponseEntity.ok(premisesServices.getPremises(premissIds));
}
@PutMapping("/start")
@PutMapping({"/start", "/start/"})
public ResponseEntity<Integer> startCalculation(@RequestBody List<Integer> premiseIds) {
return ResponseEntity.ok(premisesServices.startCalculation(premiseIds));
}
@ -81,50 +129,50 @@ 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<CalculationStatus> getUploadStatus(@PathVariable("processing_id") Integer id) {
@GetMapping({"/status/{processing_id}", "/status/{processing_id}/"})
public ResponseEntity<CalculationStatus> getCalculationStatus(@PathVariable("processing_id") Integer id) {
return ResponseEntity.ok(premisesServices.getCalculationStatus(id));
}
@PutMapping("/packaging")
@PutMapping({"/status/{processing_id}", "/status/{processing_id}/"})
public ResponseEntity<HashMap<String, String>> updatePackaging(@RequestBody PackagingUpdateDTO packagingDTO) {
return ResponseEntity.ok(premisesServices.updatePackaging(packagingDTO));
}
@PostMapping("/material")
@PostMapping({"/material", "/material/"})
public ResponseEntity<HashMap<String, String>> updateMaterial(@RequestBody MaterialUpdateDTO materialUpdateDTO) {
return ResponseEntity.ok(premisesServices.updateMaterial(materialUpdateDTO));
}
@PutMapping("/price")
@PutMapping({"/price", "/price/"})
public ResponseEntity<HashMap<String, String>> updatePrice(@RequestBody PriceUpdateDTO priceUpdateDTO) {
return ResponseEntity.ok(premisesServices.updatePrice(priceUpdateDTO));
}
@PostMapping("/destination")
@PostMapping({"/destination", "/destination/"})
public ResponseEntity<Map<Integer, DestinationDTO>> createDestination(@RequestBody DestinationCreateDTO destinationCreateDTO) {
return ResponseEntity.ok(destinationService.createDestination(destinationCreateDTO));
}
@GetMapping("/destination({id}")
@GetMapping({"/destination({id}", "/destination({id}/"})
public ResponseEntity<DestinationDTO> getDestination(@PathVariable Integer id) {
return ResponseEntity.ok(destinationService.getDestination(id));
}
@PutMapping("/destination({id}")
@PutMapping({"/destination({id}", "/destination({id}/"})
public ResponseEntity<Void> 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<Void> deleteDestination(@PathVariable Integer id) {
destinationService.deleteDestinationById(id, false);
return ResponseEntity.ok().build();
}
@PutMapping("/supplier")
@PutMapping({"/supplier", "/supplier/"})
public ResponseEntity<List<PremiseDetailDTO>> setSupplier(@RequestBody SetDataDTO setSupplierDTO) {
return ResponseEntity.ok(changeSupplierService.setSupplier(setSupplierDTO));
}

View file

@ -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<List<NodeDTO>> listNodes(@RequestParam(required = false) String filter, @RequestParam(required = false) int page, @RequestParam(required = false) int limit) {
public ResponseEntity<List<NodeDTO>> 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<NodeDTO> nodes = nodeService.listNodes(filter, page, limit);
@ -44,7 +47,7 @@ public class NodeController {
}
@GetMapping("/search")
public ResponseEntity<List<NodeDTO>> 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<List<NodeDTO>> 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<Integer> updateNode(@PathVariable Integer id, @RequestBody NodeUpdateDTO node) {
Check.equals(id, node.getId());
return ResponseEntity.ok(nodeService.updateNode(node));
}
// @PutMapping("/{id}")
// public ResponseEntity<Integer> updateNode(@PathVariable Integer id, @RequestBody NodeUpdateDTO node) {
// Check.equals(id, node.getId());
// return ResponseEntity.ok(nodeService.updateNode(node));
// }
@GetMapping("/locate")
public ResponseEntity<LocateNodeDTO> locateNode(@RequestParam String address) {
@ -70,7 +73,7 @@ public class NodeController {
}
@PutMapping("/")
public ResponseEntity<Void> addUserNode(@RequestParam AddUserNodeDTO node) {
public ResponseEntity<Void> addUserNode(@RequestBody AddUserNodeDTO node) {
userNodeService.addUserNode(node);
return ResponseEntity.ok().build();
}

View file

@ -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<List<ContainerRateDTO>> 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<List<MatrixRateDTO>> 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) {

View file

@ -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<List<GroupDTO>> listGroups(@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "1") int page) {
public ResponseEntity<List<GroupDTO>> listGroups(@RequestParam(defaultValue = "20") @Min(1) int limit,
@RequestParam(defaultValue = "1") @Min(1) int page) {
SearchQueryResult<GroupDTO> groups = groupService.listGroups(page, limit);

View file

@ -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<List<UserDTO>> 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<UserDTO> users = userService.listUsers(page, limit);

View file

@ -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;
}

View file

@ -24,6 +24,74 @@ public class NodeUpdateDTO {
private List<CountryDTO> 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<NodeType> getTypes() {
return types;
}
public void setTypes(List<NodeType> 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<Integer, NodeDTO> getPredecessors() {
return predecessors;
}
public void setPredecessors(Map<Integer, NodeDTO> predecessors) {
this.predecessors = predecessors;
}
public List<CountryDTO> getOutboundCountries() {
return outboundCountries;
}
public void setOutboundCountries(List<CountryDTO> outboundCountries) {
this.outboundCountries = outboundCountries;
}
}

View file

@ -23,6 +23,9 @@ public class NodeDetailDTO {
@JsonProperty("outbound_countries")
private List<CountryDTO> 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<CountryDTO> outboundCountries) {
this.outboundCountries = outboundCountries;
}
public void setExternalMappingId(String externalMappingId) {
this.externalMappingId = externalMappingId;
}
public String getExternalMappingId() {
return externalMappingId;
}
}

View file

@ -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() {

View file

@ -1,5 +1,7 @@
package de.avatic.lcc.dto.generic;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Objects;
/**

View file

@ -13,12 +13,16 @@ public class NodeDTO {
private List<NodeType> 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;
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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<Integer> findMissingIds(List<Integer> 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<Integer> 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<Material> {
@Override

View file

@ -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<Node> getByIds(List<Integer> 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<Map<Integer, Integer>> 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<Integer> 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<Integer> update(Node chain) {
public Optional<Integer> 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<Node> 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<Node> 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();
}

View file

@ -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);

View file

@ -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<Integer> premiseIds, Integer userId, PackagingDimension dimensionEntity, Boolean stackable, Boolean mixable) {
public void updatePackaging(List<Integer> 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<Integer> 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<Premise> 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<Integer> 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<Integer> 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<Integer> 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<Premise> 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<Premise> getPremisesByMaterialIdsAndSupplierIds(List<Integer> materialIds, List<Integer> supplierIds, List<Integer> 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<Integer> findAssociatedUserSuppliers(List<Integer> materialIds) {
public List<Integer> findAssociatedUserSuppliers(List<Integer> materialIds, Integer userId) {
if (materialIds == null || materialIds.isEmpty()) {
return Collections.emptyList();
}
List<Object> 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);
}
}
}
@ -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;
}
}

View file

@ -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<RouteNode> 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;

View file

@ -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<ContainerRate> 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<ContainerRate> listAllRatesByPeriodId(Integer periodId) {

View file

@ -56,7 +56,7 @@ public class MatrixRateRepository {
@Transactional
public SearchQueryResult<MatrixRate> 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;
}

View file

@ -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<Integer> 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<ValidityPeriod> 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

View file

@ -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<Node> getById(Integer id) {
@ -65,6 +86,30 @@ public class UserNodeRepository {
}
public Collection<Node> getByIds(List<Integer> 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<Integer> 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<Node> {
@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;
}

View file

@ -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
}

View file

@ -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<ContainerRateDTO> 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))));
}
/**

View file

@ -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<MatrixRateDTO> 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);
}
/**

View file

@ -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()));
}
/**

View file

@ -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<>();

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -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<PremiseDetailDTO> createPremises(List<Integer> materialIds, List<Integer> supplierIds, List<Integer> userSupplierIds, boolean createEmpty) {
Integer userId = 1; //TODO get user id
List<TemporaryPremise> foundPremises = new ArrayList<>(premiseRepository.getPremisesByMaterialIdsAndSupplierIds(materialIds, supplierIds, userSupplierIds, userId, createEmpty).stream().map(TemporaryPremise::new).toList());
/* Build all resulting premises */
List<TemporaryPremise> 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<TemporaryPremise> 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<TemporaryPremise> toBeCreated = materialIds.stream()
.flatMap(materialId -> {
Stream<TemporaryPremise> supplierCombinations = supplierIds.stream()
.map(supplierId -> new TemporaryPremise(materialId, supplierId, null, false));
premises.forEach(p -> verifyNode(p, userId));
verifyMaterial(materialIds);
Stream<TemporaryPremise> 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<Integer> premiseIds = new ArrayList<>(toBeCreated.stream().map(p -> premiseRepository.insert(p.getMaterialId(), p.getSupplierId(), p.getUserSupplierId(), userId)).toList());
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.DRAFT)) { // recycle
p.setId(p.getPremise().getId());
if (createEmpty) {
// reset to defaults.
fillPremise(p, userId);
}
return premiseRepository.getPremisesById(premiseIds).stream().map(premiseTransformer::toPremiseDetailDTO).toList();
} 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(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<PackagingDimension> hu = packagingDimensionRepository.getById(packaging.getFirst().getHuId());
Optional<PackagingDimension> 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<Integer> 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<Premise> 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;
}
}
}

View file

@ -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> material = materialRepository.getByPartNumbers(findPartNumbers(search));
List<Integer> materialIds = material.stream().map(Material::getId).toList();
// find suppliers associated with this material.
List<Integer> supplierIds = premiseRepository.findAssociatedSuppliers(materialIds);
List<Integer> userSupplierIds = premiseRepository.findAssociatedUserSuppliers(materialIds);
List<Integer> 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;
}

View file

@ -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<SectionInfo> getCustomRelevantRouteSections(List<SectionInfo> sections) {

View file

@ -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());

View file

@ -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;
}

View file

@ -37,7 +37,9 @@ public class NodeDetailTransformer {
var chains = new ArrayList<Map<Integer, NodeDTO>>();
for (var chain : node.getNodePredecessors()) {
var foundChains = node.getNodePredecessors();
for (var chain : foundChains) {
Map<Integer, NodeDTO> 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;
}

View file

@ -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;
}
}

View file

@ -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());
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);
}

View file

@ -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);
}
}

View file

@ -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);
}
@ -34,4 +20,26 @@ public class NotFoundException extends BadRequestException {
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;
}
}
}

View file

@ -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;
}
}

File diff suppressed because it is too large Load diff

View file

@ -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 (

File diff suppressed because it is too large Load diff

View file

@ -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`)
@ -144,8 +144,8 @@ CREATE TABLE IF NOT EXISTS node
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),
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,10 +190,10 @@ 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,
@ -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',
@ -332,18 +332,18 @@ 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),
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)',
@ -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,
@ -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,

View file

@ -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<Map<Integer, Integer>> 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<Integer, Integer> predecessors = new HashMap<>();
while (rs.next()) {
predecessors.put(rs.getInt("sequence_number"), rs.getInt("predecessor_node_id"));
}
return predecessors;
}, chainId);
}, id);
}
private Collection<Integer> 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<Node> {
@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;
}
}
}

View file

@ -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 = "<script>alert('xss')</script>";
mockMvc.perform(get("/api/nodes/")
.param("filter", xssAttempt))
.andExpect(status().isOk()) // Should not fail but should sanitize
.andExpect(jsonPath("$[*].address", not(containsString("<script>"))));
}
@Test
@DisplayName("Should handle SQL injection attempts in filter")
void shouldHandleSqlInjectionAttemptsInFilter() throws Exception {
String sqlInjection = "'; DROP TABLE node; --";
mockMvc.perform(get("/api/nodes/")
.param("filter", sqlInjection))
.andExpect(status().isOk()); // Should not cause database issues
}
}
@Nested
@DisplayName("Performance Tests")
class PerformanceTests {
@Test
@DisplayName("Should handle large page sizes efficiently")
@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 shouldHandleLargePageSizesEfficiently() throws Exception {
long startTime = System.currentTimeMillis();
mockMvc.perform(get("/api/nodes/")
.param("limit", "1000"))
.andExpect(status().isOk());
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// Assert that the request completes within reasonable time (adjust as needed)
assert executionTime < 5000; // 5 seconds max
}
}
}

View file

@ -0,0 +1,262 @@
package de.avatic.lcc.controller.configuration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
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 java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* Advanced integration tests for RateController covering edge cases and complex scenarios
*/
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class RateControllerAdvancedIntegrationTest {
@Autowired
private MockMvc mockMvc;
private static final String BASE_URL = "/api/rates";
// Parameterized Tests
@ParameterizedTest
@ValueSource(strings = {"container", "matrix"})
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testRateEndpoints_WithDifferentLimits(String endpoint) throws Exception {
int[] limits = {1, 5, 10, 20, 50};
for (int limit : limits) {
mockMvc.perform(get(BASE_URL + "/" + endpoint)
.param("limit", String.valueOf(limit))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(lessThanOrEqualTo(limit))));
}
}
@ParameterizedTest
@CsvSource({
"container,RAIL",
"container,SEA",
"container,ROAD",
"container,POST-RUN"
})
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testContainerRates_FilterByTransportType(String endpoint, String transportType) throws Exception {
mockMvc.perform(get(BASE_URL + "/" + endpoint)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$[?(@.type == '" + transportType + "')]").exists());
}
// Complex Date Range Tests
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testContainerRates_WithFutureDateFilter_ShouldReturnOnlyFutureValidRates() throws Exception {
LocalDateTime futureDate = LocalDateTime.now().plusMonths(6);
String formattedDate = futureDate.format(DateTimeFormatter.ISO_DATE_TIME);
mockMvc.perform(get(BASE_URL + "/container")
.param("validAt", formattedDate)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$[*].validity_period.state", everyItem(equalTo("VALID"))));
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testContainerRates_WithPastDateFilter_ShouldReturnHistoricalRates() throws Exception {
LocalDateTime pastDate = LocalDateTime.now().minusMonths(18);
String formattedDate = pastDate.format(DateTimeFormatter.ISO_DATE_TIME);
mockMvc.perform(get(BASE_URL + "/container")
.param("validAt", formattedDate)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
// Concurrent Request Tests
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testContainerRates_MultipleFiltersSimultaneously_ShouldPrioritizeValidAt() throws Exception {
LocalDateTime validAt = LocalDateTime.now();
String formattedDate = validAt.format(DateTimeFormatter.ISO_DATE_TIME);
// When both validAt and valid parameters are provided, validAt should take precedence
mockMvc.perform(get(BASE_URL + "/container")
.param("validAt", formattedDate)
.param("valid", "2") // This should be ignored
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
// State Transition Tests
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testValidityPeriod_StateTransitions_DraftToValid() throws Exception {
// First check if there are draft rates
mockMvc.perform(get(BASE_URL + "/staged_changes")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$").value(true));
// Approve drafts
mockMvc.perform(put(BASE_URL + "/staged_changes")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
// Verify no more drafts exist
mockMvc.perform(get(BASE_URL + "/staged_changes")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$").value(false));
}
// Edge Cases for Pagination
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testPagination_RequestingPageBeyondAvailable_ShouldReturnEmptyList() throws Exception {
mockMvc.perform(get(BASE_URL + "/container")
.param("limit", "100")
.param("page", "999")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(0)))
.andExpect(header().string("X-Current-Page", "999"));
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testPagination_WithZeroLimit_ShouldUseDefaultLimit() throws Exception {
mockMvc.perform(get(BASE_URL + "/container")
.param("limit", "0")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(greaterThan(0))));
}
// Complex Filtering Scenarios
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testMatrixRates_FilterBySpecificCountryPairs() throws Exception {
mockMvc.perform(get(BASE_URL + "/matrix")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$[?(@.origin.country.iso_code == 'DE' && @.destination.country.iso_code == 'FR')]").exists());
}
// Business Logic Validation Tests
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testContainerRates_ValidateRateHierarchy() throws Exception {
mockMvc.perform(get(BASE_URL + "/container")
.param("limit", "100")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
// 40ft container rate should be less than 40HC rate
.andExpect(jsonPath("$[*]", everyItem(
allOf(
hasEntry(equalTo("rates"),
allOf(
hasKey("40"),
hasKey("40_HC")
)
)
)
)));
}
// Null and Empty Value Handling
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testValidityPeriods_WithNullEndDates_ShouldHandleGracefully() throws Exception {
mockMvc.perform(get(BASE_URL + "/periods")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$[?(@.end_date == null)]").exists());
}
// Special Character Handling
@Test
void testInvalidParameters_WithSpecialCharacters_ShouldReturn400() throws Exception {
mockMvc.perform(get(BASE_URL + "/container")
.param("limit", "20'; DROP TABLE container_rate; --")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
// Large Dataset Performance Test
@Test
@Sql(scripts = {"classpath:master_data/rates-large-dataset.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testPerformance_WithLargeDataset_ShouldMaintainResponseTime() throws Exception {
long startTime = System.currentTimeMillis();
// Test with pagination to ensure efficient querying
mockMvc.perform(get(BASE_URL + "/container")
.param("limit", "50")
.param("page", "10")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// Even with large dataset, paginated request should be fast
assert duration < 1000 : "Request took too long with large dataset: " + duration + "ms";
}
// Idempotency Tests
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testInvalidatePeriod_MultipleCallsToSameId_ShouldBeIdempotent() throws Exception {
Integer periodId = 4; // Using invalid period from test data
// First call
mockMvc.perform(delete(BASE_URL + "/periods/{id}", periodId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
// Second call - should still return OK (idempotent)
mockMvc.perform(delete(BASE_URL + "/periods/{id}", periodId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}

View file

@ -0,0 +1,370 @@
package de.avatic.lcc.controller.configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.avatic.lcc.dto.configuration.matrixrates.MatrixRateDTO;
import de.avatic.lcc.dto.configuration.rates.ContainerRateDTO;
import de.avatic.lcc.dto.generic.ValidityPeriodDTO;
import de.avatic.lcc.model.rates.ValidityPeriodState;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
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.test.web.servlet.MvcResult;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
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 RateControllerIntegrationTest {
private static final String BASE_URL = "/api/rates";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
// Container Rate Tests
@Nested
@DisplayName("/api/rates/container - List container rates")
class ListContainerRatesTest {
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListContainerRates_WithoutFilter_ShouldReturnAllRates() throws Exception {
mockMvc.perform(get(BASE_URL + "/container")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(greaterThan(0))))
.andExpect(header().exists("X-Total-Count"))
.andExpect(header().exists("X-Page-Count"))
.andExpect(header().string("X-Current-Page", "1"))
.andExpect(jsonPath("$[0].id").exists())
.andExpect(jsonPath("$[0].origin").exists())
.andExpect(jsonPath("$[0].destination").exists())
.andExpect(jsonPath("$[0].type").exists())
.andExpect(jsonPath("$[0].rates").exists())
.andExpect(jsonPath("$[0].lead_time").exists())
.andExpect(jsonPath("$[0].validity_period").exists());
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListContainerRates_WithPagination_ShouldReturnPagedResults() throws Exception {
mockMvc.perform(get(BASE_URL + "/container")
.param("limit", "5")
.param("page", "1")
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(equalTo(5))))
.andExpect(header().string("X-Current-Page", "1"));
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListContainerRates_WithValidityPeriodFilter_ShouldReturnFilteredRates() throws Exception {
// Assuming validity period ID 1 exists in test data
mockMvc.perform(get(BASE_URL + "/container")
.param("valid", "1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$[*].validity_period.id", everyItem(equalTo(1))));
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListContainerRates_WithValidAtFilter_ShouldReturnValidRates() throws Exception {
LocalDateTime validAt = LocalDateTime.now();
String formattedDate = validAt.format(DateTimeFormatter.ISO_DATE_TIME);
mockMvc.perform(get(BASE_URL + "/container")
.param("validAt", formattedDate)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
// Container Rate Tests
@Nested
@DisplayName("/api/rates/container/id - get container rate detail")
class GetContainerRatesTest {
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testGetContainerRate_WithValidId_ShouldReturnRate() throws Exception {
// First, get a valid ID from the list
MvcResult listResult = mockMvc.perform(get(BASE_URL + "/container")
.param("limit", "1"))
.andExpect(status().isOk())
.andReturn();
List<ContainerRateDTO> rates = objectMapper.readValue(
listResult.getResponse().getContentAsString(),
objectMapper.getTypeFactory().constructCollectionType(List.class, ContainerRateDTO.class)
);
if (!rates.isEmpty()) {
Integer rateId = rates.getFirst().getId();
mockMvc.perform(get(BASE_URL + "/container/{id}", rateId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(rateId))
.andExpect(jsonPath("$.origin").exists())
.andExpect(jsonPath("$.destination").exists())
.andExpect(jsonPath("$.type").exists())
.andExpect(jsonPath("$.rates.FEU").exists())
.andExpect(jsonPath("$.rates.TEU").exists())
.andExpect(jsonPath("$.rates.HQ").exists());
}
}
@Test
void testGetContainerRate_WithInvalidId_ShouldReturn400() throws Exception {
mockMvc.perform(get(BASE_URL + "/container/{id}", 99999)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
}
// Matrix Rate Tests
@Nested
@DisplayName("/api/rates/matrix/ - list matrix rates")
class ListMatrixRatesTest {
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListMatrixRates_WithoutFilter_ShouldReturnAllRates() throws Exception {
mockMvc.perform(get(BASE_URL + "/matrix")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(0))))
.andExpect(header().exists("X-Total-Count"))
.andExpect(header().exists("X-Page-Count"))
.andExpect(header().string("X-Current-Page", "1"));
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListMatrixRates_WithValidFilter_ShouldReturnFilteredRates() throws Exception {
mockMvc.perform(get(BASE_URL + "/matrix")
.param("valid", "1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
@Nested
@DisplayName("/api/rates/matrix/id - get matrix rate detail")
class GetMatrixRatesTest {
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testGetMatrixRate_WithValidId_ShouldReturnRate() throws Exception {
// First, get a valid ID from the list
MvcResult listResult = mockMvc.perform(get(BASE_URL + "/matrix")
.param("limit", "1"))
.andExpect(status().isOk())
.andReturn();
List<MatrixRateDTO> rates = objectMapper.readValue(
listResult.getResponse().getContentAsString(),
objectMapper.getTypeFactory().constructCollectionType(List.class, MatrixRateDTO.class)
);
if (!rates.isEmpty()) {
Integer rateId = rates.getFirst().getId();
mockMvc.perform(get(BASE_URL + "/matrix/{id}", rateId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(rateId))
.andExpect(jsonPath("$.origin").exists())
.andExpect(jsonPath("$.destination").exists())
.andExpect(jsonPath("$.rate").exists());
}
}
}
// Validity Period Tests
@Nested
@DisplayName("/api/rates/periods - List validity periods")
class ListPeriodsTest {
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListPeriods_ShouldReturnAllPeriods() throws Exception {
mockMvc.perform(get(BASE_URL + "/periods")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(0))));
}
}
@Nested
@DisplayName("/api/rates/periods/id - Invalidate validity periods")
class GetPeriodsTest {
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testInvalidatePeriod_WithValidId_ShouldReturnOk() throws Exception {
// First get a period to invalidate
MvcResult listResult = mockMvc.perform(get(BASE_URL + "/periods"))
.andExpect(status().isOk())
.andReturn();
List<ValidityPeriodDTO> periods = objectMapper.readValue(
listResult.getResponse().getContentAsString(),
objectMapper.getTypeFactory().constructCollectionType(List.class, ValidityPeriodDTO.class)
);
assertThat(periods).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();
Integer periodId = periods.stream().filter(p -> p.getState().equals(ValidityPeriodState.EXPIRED)).findFirst().orElseThrow(() -> new AssertionError("No expired periods found")).getId();
mockMvc.perform(delete(BASE_URL + "/periods/{id}", periodId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
listResult = mockMvc.perform(get(BASE_URL + "/periods"))
.andExpect(status().isOk())
.andReturn();
periods = objectMapper.readValue(
listResult.getResponse().getContentAsString(),
objectMapper.getTypeFactory().constructCollectionType(List.class, ValidityPeriodDTO.class)
);
assertThat(periods).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty();
assertThat(periods.stream().filter(p -> p.getId().equals(periodId)).findFirst().orElseThrow(() -> new AssertionError("Periods with id " + periodId + " not found"))).extracting(ValidityPeriodDTO::getState).isEqualTo(ValidityPeriodState.INVALID);
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testInvalidatePeriod_WithInvalidId_ShouldReturn404() throws Exception {
mockMvc.perform(delete(BASE_URL + "/periods/{id}", 99999)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
}
// Staged Changes Tests
@Nested
@DisplayName("/api/rates/staged_changes - staged changes")
class StagedChangesTest {
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testCheckRateDrafts_ShouldReturnBoolean() throws Exception {
mockMvc.perform(get(BASE_URL + "/staged_changes")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(print())
.andExpect(jsonPath("$").isBoolean())
.andExpect(jsonPath("$").value(true));
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testApproveRateDrafts_ShouldReturnOk() throws Exception {
mockMvc.perform(put(BASE_URL + "/staged_changes")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
// Edge Cases and Error Handling Tests
@Nested
@DisplayName("/api/rates/ - Edge cases")
class EdgeCasesTest {
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListContainerRates_WithInvalidDateFormat_ShouldReturn400() throws Exception {
mockMvc.perform(get(BASE_URL + "/container")
.param("validAt", "invalid-date")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListContainerRates_WithNegativeLimit_ShouldReturn400() throws Exception {
mockMvc.perform(get(BASE_URL + "/container")
.param("limit", "-1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListMatrixRates_WithNegativePage_ShouldReturn400() throws Exception {
mockMvc.perform(get(BASE_URL + "/matrix")
.param("page", "-1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
}
}
// Performance Tests
@Nested
@DisplayName("/api/rates/ - Performance tests")
class PerformanceTests {
@Test
@Sql(scripts = {"classpath:master_data/reduced_rate_setup.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:master_data/reduced_rate_setup-cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testListContainerRates_LargeLimit_ShouldCompleteInReasonableTime() throws Exception {
long startTime = System.currentTimeMillis();
mockMvc.perform(get(BASE_URL + "/container")
.param("limit", "1000")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// Assert that the request completes within 5 seconds
assertTrue(duration < 5000, "Request took too long: " + duration + "ms");
}
}
}

View file

@ -0,0 +1,76 @@
-- cleanup of full setup data in alldata.sql
delete from container_rate where 1;
ALTER TABLE container_rate AUTO_INCREMENT = 1;
delete from country_matrix_rate where 1;
ALTER TABLE country_matrix_rate AUTO_INCREMENT = 1;
delete from validity_period where 1;
ALTER TABLE validity_period AUTO_INCREMENT = 1;
delete
from packaging_property
where 1;
delete
from packaging_property_type
where 1;
delete
from packaging
where 1;
delete
from packaging_dimension
where 1;
delete
from material
where 1;
ALTER TABLE packaging_property AUTO_INCREMENT = 1;
ALTER TABLE packaging_property_type AUTO_INCREMENT = 1;
ALTER TABLE packaging AUTO_INCREMENT = 1;
ALTER TABLE packaging_dimension AUTO_INCREMENT = 1;
ALTER TABLE material AUTO_INCREMENT = 1;
delete
from node_predecessor_entry
where 1;
delete
from node_predecessor_chain
where 1;
delete
from node
where 1;
ALTER TABLE node AUTO_INCREMENT = 1;
ALTER TABLE node_predecessor_chain AUTO_INCREMENT = 1;
ALTER TABLE node_predecessor_entry AUTO_INCREMENT = 1;
delete
from country_property
where 1;
delete
from country_property_type
where 1;
delete
from country
where 1;
delete
from system_property
where 1;
delete
from system_property_type
where 1;
delete
from property_set
where 1;
ALTER TABLE country_property AUTO_INCREMENT = 1;
ALTER TABLE country_property_type AUTO_INCREMENT = 1;
ALTER TABLE country AUTO_INCREMENT = 1;
ALTER TABLE system_property AUTO_INCREMENT = 1;
ALTER TABLE system_property_type AUTO_INCREMENT = 1;
ALTER TABLE property_set AUTO_INCREMENT = 1;

File diff suppressed because it is too large Load diff

View file

@ -16,3 +16,11 @@ where 1;
delete
from lcc_test.property_set
where 1;
ALTER TABLE lcc_test.country_property AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.country_property_type AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.country AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.system_property AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.system_property_type AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.property_set AUTO_INCREMENT = 1;

View file

@ -13,3 +13,11 @@ where 1;
delete
from lcc_test.material
where 1;
ALTER TABLE lcc_test.packaging_property AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.packaging_property_type AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.packaging AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.packaging_dimension AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.material AUTO_INCREMENT = 1;

View file

@ -296,7 +296,7 @@ SET @packaging_3064540201 = LAST_INSERT_ID();
-- Part Number: 8212640113
INSERT INTO packaging (supplier_node_id, material_id, hu_dimension_id, shu_dimension_id, is_deprecated)
VALUES (
(SELECT id FROM node WHERE name = 'Linde (China) Forklift Truck (Supplier)' LIMIT 1),
(SELECT id FROM node WHERE name = 'KION Baoli (Jiangsu) Forklift Co., Ltd.' LIMIT 1),
(SELECT id FROM material WHERE part_number = '8212640113' LIMIT 1),
@hu_dim_8212640113,
@shu_dim_8212640113,

View file

@ -7,3 +7,8 @@ where 1;
delete
from lcc_test.node
where 1;
ALTER TABLE lcc_test.node AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.node_predecessor_chain AUTO_INCREMENT = 1;
ALTER TABLE lcc_test.node_predecessor_entry AUTO_INCREMENT = 1;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
delete from premise_route_section where 1;
delete from premise_route_node where 1;
delete from premise_route where 1;
delete from premise_destination where 1;
delete from premise where 1;
ALTER TABLE premise AUTO_INCREMENT = 1;
ALTER TABLE premise_destination AUTO_INCREMENT = 1;
ALTER TABLE premise_route AUTO_INCREMENT = 1;
ALTER TABLE premise_route_section AUTO_INCREMENT = 1;
ALTER TABLE premise_route_node AUTO_INCREMENT = 1;

View file

@ -0,0 +1,458 @@
-- ============================================
-- Premise Data Generation Script
-- ============================================
-- This script creates fake users and premises based on existing materials, nodes, and packaging
-- ============================================
-- Insert Premises using existing materials and packaging
-- ============================================
START TRANSACTION;
-- Premise 1: Gearbox housing blank from Linde China | john.doe@company.com | COMPLETED (0-Month-old)
INSERT INTO premise (material_id,
supplier_node_id,
geo_lat,
geo_lng,
country_id,
packaging_id,
user_id,
material_cost,
is_fca_enabled,
oversea_share,
hs_code,
tariff_rate,
state,
individual_hu_length,
individual_hu_height,
individual_hu_width,
individual_hu_weight,
hu_displayed_dimension_unit,
hu_displayed_weight_unit,
hu_unit_count,
hu_stackable,
hu_mixable,
updated_at)
VALUES ((SELECT id FROM material WHERE part_number = '28152640129'),
(SELECT id FROM node WHERE external_mapping_id = 'LX'),
24.489,
118.1478,
(SELECT id FROM country WHERE iso_code = 'CN'),
(SELECT id FROM packaging WHERE material_id = (SELECT id FROM material WHERE part_number = '28152640129')),
(SELECT id FROM sys_user WHERE email = 'john.doe@company.com'),
2850.50,
TRUE,
0.75,
'84839089',
0.065,
'COMPLETED',
1200,
400,
700,
677000,
'MM',
'G',
2,
TRUE,
FALSE,
NOW());
SET @premise_id_1 = LAST_INSERT_ID();
-- Premise 2: Gearbox housing blank from Linde China | john.doe@company.com | COMPLETED (3-Month-old)
INSERT INTO premise (material_id,
supplier_node_id,
geo_lat,
geo_lng,
country_id,
packaging_id,
user_id,
material_cost,
is_fca_enabled,
oversea_share,
hs_code,
tariff_rate,
state,
individual_hu_length,
individual_hu_height,
individual_hu_width,
individual_hu_weight,
hu_displayed_dimension_unit,
hu_displayed_weight_unit,
hu_unit_count,
hu_stackable,
hu_mixable,
updated_at)
VALUES ((SELECT id FROM material WHERE part_number = '28152640129'),
(SELECT id FROM node WHERE external_mapping_id = 'LX'),
24.489,
118.1478,
(SELECT id FROM country WHERE iso_code = 'CN'),
(SELECT id FROM packaging WHERE material_id = (SELECT id FROM material WHERE part_number = '28152640129')),
(SELECT id FROM sys_user WHERE email = 'john.doe@company.com'),
2850.50,
TRUE,
0.75,
'84839089',
0.065,
'COMPLETED',
1200,
790,
700,
677000,
'MM',
'G',
1,
TRUE,
FALSE,
DATE_SUB(NOW(), INTERVAL 3 MONTH));
SET @premiseId2 = LAST_INSERT_ID();
-- Premise 3: planet gear carrier blank 'stage 1 from Linde China | john.doe@company.com | DRAFT
INSERT INTO premise (material_id,
supplier_node_id,
geo_lat,
geo_lng,
country_id,
packaging_id,
user_id,
material_cost,
is_fca_enabled,
oversea_share,
hs_code,
tariff_rate,
state,
individual_hu_length,
individual_hu_height,
individual_hu_width,
individual_hu_weight,
hu_displayed_dimension_unit,
hu_displayed_weight_unit,
hu_unit_count,
hu_stackable,
hu_mixable,
updated_at)
VALUES ((SELECT id FROM material WHERE part_number = '8222640822'),
(SELECT id FROM node WHERE external_mapping_id = 'LX'),
24.489,
118.1478,
(SELECT id FROM country WHERE iso_code = 'CN'),
(SELECT id FROM packaging WHERE material_id = (SELECT id FROM material WHERE part_number = '8222640822')),
(SELECT id FROM sys_user WHERE email = 'john.doe@company.com'),
2850.50,
TRUE,
0.75,
'84839089',
0.065,
'DRAFT',
1200,
790,
700,
677000,
'MM',
'G',
3,
TRUE,
FALSE,
NOW());
SET @premiseId3 = LAST_INSERT_ID();
-- Premise 4: planet gear carrier blank 'stage 1 from Linde China | john.doe@company.com | COMPLETED
INSERT INTO premise (material_id,
supplier_node_id,
geo_lat,
geo_lng,
country_id,
packaging_id,
user_id,
material_cost,
is_fca_enabled,
oversea_share,
hs_code,
tariff_rate,
state,
individual_hu_length,
individual_hu_height,
individual_hu_width,
individual_hu_weight,
hu_displayed_dimension_unit,
hu_displayed_weight_unit,
hu_unit_count,
hu_stackable,
hu_mixable,
updated_at)
VALUES ((SELECT id FROM material WHERE part_number = '8222640822'),
(SELECT id FROM node WHERE external_mapping_id = 'LX'),
24.489,
118.1478,
(SELECT id FROM country WHERE iso_code = 'CN'),
(SELECT id FROM packaging WHERE material_id = (SELECT id FROM material WHERE part_number = '8222640822')),
(SELECT id FROM sys_user WHERE email = 'john.doe@company.com'),
2850.50,
TRUE,
0.75,
'84839089',
0.065,
'COMPLETED',
1200,
790,
700,
677000,
'MM',
'G',
1,
TRUE,
FALSE,
NOW());
SET @premiseId4 = LAST_INSERT_ID();
-- Premise 5: planet gear carrier blank 'stage 1 from Linde China | sarah.smith@company.com | DRAFT
INSERT INTO premise (material_id,
supplier_node_id,
geo_lat,
geo_lng,
country_id,
packaging_id,
user_id,
material_cost,
is_fca_enabled,
oversea_share,
hs_code,
tariff_rate,
state,
individual_hu_length,
individual_hu_height,
individual_hu_width,
individual_hu_weight,
hu_displayed_dimension_unit,
hu_displayed_weight_unit,
hu_unit_count,
hu_stackable,
hu_mixable,
updated_at)
VALUES ((SELECT id FROM material WHERE part_number = '8222640822'),
(SELECT id FROM node WHERE external_mapping_id = 'LX'),
24.489,
118.1478,
(SELECT id FROM country WHERE iso_code = 'CN'),
(SELECT id FROM packaging WHERE material_id = (SELECT id FROM material WHERE part_number = '8222640822')),
(SELECT id FROM sys_user WHERE email = 'sarah.smith@company.com'),
2850.50,
TRUE,
0.75,
'84839089',
0.065,
'DRAFT',
1200,
790,
700,
677000,
'MM',
'KG',
1,
TRUE,
FALSE,
NOW());
SET @premiseId5 = LAST_INSERT_ID();
-- Premise 6: wheel hub from Linde China | sarah.smith@company.com | DRAFT
INSERT INTO premise (material_id,
supplier_node_id,
geo_lat,
geo_lng,
country_id,
packaging_id,
user_id,
material_cost,
is_fca_enabled,
oversea_share,
hs_code,
tariff_rate,
state,
individual_hu_length,
individual_hu_height,
individual_hu_width,
individual_hu_weight,
hu_displayed_dimension_unit,
hu_displayed_weight_unit,
hu_unit_count,
hu_stackable,
hu_mixable,
updated_at)
VALUES ((SELECT id FROM material WHERE part_number = '3064540201'),
(SELECT id FROM node WHERE external_mapping_id = 'LX'),
24.489,
118.1478,
(SELECT id FROM country WHERE iso_code = 'CN'),
(SELECT id FROM packaging WHERE material_id = (SELECT id FROM material WHERE part_number = '3064540201')),
(SELECT id FROM sys_user WHERE email = 'sarah.smith@company.com'),
2850.50,
TRUE,
0.75,
'84839089',
0.065,
'DRAFT',
1200,
790,
700,
677000,
'MM',
'KG',
1,
TRUE,
FALSE,
NOW());
SET @premiseId6 = LAST_INSERT_ID();
-- Premise 7: wheel hub from Linde China | sarah.smith@company.com | COMPLETED
INSERT INTO premise (material_id,
supplier_node_id,
geo_lat,
geo_lng,
country_id,
packaging_id,
user_id,
material_cost,
is_fca_enabled,
oversea_share,
hs_code,
tariff_rate,
state,
individual_hu_length,
individual_hu_height,
individual_hu_width,
individual_hu_weight,
hu_displayed_dimension_unit,
hu_displayed_weight_unit,
hu_unit_count,
hu_stackable,
hu_mixable,
updated_at)
VALUES ((SELECT id FROM material WHERE part_number = '3064540201'),
(SELECT id FROM node WHERE external_mapping_id = 'LX'),
24.489,
118.1478,
(SELECT id FROM country WHERE iso_code = 'CN'),
(SELECT id FROM packaging WHERE material_id = (SELECT id FROM material WHERE part_number = '3064540201')),
(SELECT id FROM sys_user WHERE email = 'sarah.smith@company.com'),
2850.50,
TRUE,
0.75,
'84839089',
0.065,
'COMPLETED',
1200,
790,
700,
677000,
'MM',
'KG',
4,
TRUE,
FALSE,
NOW());
SET @premiseId7 = LAST_INSERT_ID();
-- Premise 8: Gearbox housing blank from Linde China | john.doe@company.com | DRAFT
INSERT INTO premise (material_id,
user_supplier_node_id,
geo_lat,
geo_lng,
country_id,
packaging_id,
user_id,
material_cost,
is_fca_enabled,
oversea_share,
hs_code,
tariff_rate,
state,
individual_hu_length,
individual_hu_height,
individual_hu_width,
individual_hu_weight,
hu_displayed_dimension_unit,
hu_displayed_weight_unit,
hu_unit_count,
hu_stackable,
hu_mixable,
updated_at)
VALUES ((SELECT id FROM material WHERE part_number = '28152640129'),
(SELECT id FROM sys_user_node WHERE name = 'My Supplier 1'),
24.489,
118.1478,
(SELECT id FROM country WHERE iso_code = 'CN'),
(SELECT id FROM packaging WHERE material_id = (SELECT id FROM material WHERE part_number = '28152640129')),
(SELECT id FROM sys_user WHERE email = 'john.doe@company.com'),
2850.50,
TRUE,
0.75,
'84839089',
0.065,
'DRAFT',
1200,
790,
700,
677000,
'MM',
'G',
8,
TRUE,
FALSE,
NOW());
SET @premiseId8 = LAST_INSERT_ID();
-- Premise 9: gearbox housing blank 'GR4H-10 from Linde China | sarah.smith@company.com | COMPLETED
INSERT INTO premise (material_id,
user_supplier_node_id,
geo_lat,
geo_lng,
country_id,
packaging_id,
user_id,
material_cost,
is_fca_enabled,
oversea_share,
hs_code,
tariff_rate,
state,
individual_hu_length,
individual_hu_height,
individual_hu_width,
individual_hu_weight,
hu_displayed_dimension_unit,
hu_displayed_weight_unit,
hu_unit_count,
hu_stackable,
hu_mixable,
updated_at)
VALUES ((SELECT id FROM material WHERE part_number = '28152640129'),
(SELECT id FROM sys_user_node WHERE name = 'My Supplier 2'),
24.489,
118.1478,
(SELECT id FROM country WHERE iso_code = 'CN'),
(SELECT id FROM packaging WHERE material_id = (SELECT id FROM material WHERE part_number = '28152640129')),
(SELECT id FROM sys_user WHERE email = 'sarah.smith@company.com'),
2850.50,
TRUE,
0.75,
'84839089',
0.065,
'COMPLETED',
1200,
790,
700,
677000,
'MM',
'G',
1,
TRUE,
FALSE,
NOW());
SET @premiseId9 = LAST_INSERT_ID();
COMMIT;

View file

@ -0,0 +1,38 @@
-- Cleanup script for RateController Integration Tests
-- (RateControllerIntegrationTest.java)
-- Delete in reverse order of foreign key dependencies
-- Delete system properties
DELETE FROM system_property WHERE property_set_id IN (1, 2);
-- Delete system property types
DELETE FROM system_property_type WHERE id IN (1, 2, 3);
-- Delete property sets
DELETE FROM property_set WHERE id IN (1, 2);
-- Delete country matrix rates
DELETE FROM country_matrix_rate WHERE validity_period_id IN (1, 2, 3, 4);
-- Delete container rates
DELETE FROM container_rate WHERE validity_period_id IN (1, 2, 3, 4);
-- Delete validity periods
DELETE FROM validity_period WHERE id IN (1, 2, 3, 4);
-- Delete nodes
DELETE FROM node WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8);
-- Delete countries
DELETE FROM country WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8);
-- Reset auto-increment counters if needed (optional)
ALTER TABLE country AUTO_INCREMENT = 1;
ALTER TABLE node AUTO_INCREMENT = 1;
ALTER TABLE validity_period AUTO_INCREMENT = 1;
ALTER TABLE container_rate AUTO_INCREMENT = 1;
ALTER TABLE country_matrix_rate AUTO_INCREMENT = 1;
ALTER TABLE property_set AUTO_INCREMENT = 1;
ALTER TABLE system_property_type AUTO_INCREMENT = 1;
ALTER TABLE system_property AUTO_INCREMENT = 1;

View file

@ -0,0 +1,95 @@
-- Reduced test data set for RateController Integration Tests
-- (RateControllerIntegrationTest.java)
-- Insert test countries
INSERT INTO country (id, iso_code, region_code, is_deprecated) VALUES
(1, 'DE', 'EMEA', FALSE),
(2, 'FR', 'EMEA', FALSE),
(3, 'IT', 'EMEA', FALSE),
(4, 'ES', 'EMEA', FALSE),
(5, 'CZ', 'EMEA', FALSE),
(6, 'PL', 'EMEA', FALSE),
(7, 'NL', 'EMEA', FALSE),
(8, 'BE', 'EMEA', FALSE);
-- Insert test nodes
INSERT INTO node (id, country_id, name, address, external_mapping_id, predecessor_required, is_destination, is_source, is_intermediate, geo_lat, geo_lng, is_deprecated) VALUES
(1, 1, 'Hamburg Port', 'Port of Hamburg, Germany', 'HAM001', FALSE, TRUE, TRUE, FALSE, 53.5511, 9.9937, FALSE),
(2, 1, 'Munich Terminal', 'Munich Rail Terminal, Germany', 'MUC001', FALSE, TRUE, TRUE, TRUE, 48.1351, 11.5820, FALSE),
(3, 1, 'Berlin Hub', 'Berlin Central Hub, Germany', 'BER001', FALSE, TRUE, TRUE, TRUE, 52.5200, 13.4050, FALSE),
(4, 2, 'Paris Terminal', 'Paris Logistics Center, France', 'PAR001', FALSE, TRUE, TRUE, TRUE, 48.8566, 2.3522, FALSE),
(5, 3, 'Milan Hub', 'Milan Distribution Center, Italy', 'MIL001', FALSE, TRUE, TRUE, TRUE, 45.4642, 9.1900, FALSE),
(6, 4, 'Barcelona Port', 'Port of Barcelona, Spain', 'BCN001', FALSE, TRUE, TRUE, FALSE, 41.3851, 2.1734, FALSE),
(7, 5, 'Prague Hub', 'Prague Distribution Center, Czech Republic', 'PRG001', FALSE, TRUE, TRUE, TRUE, 50.0755, 14.4378, FALSE),
(8, 6, 'Warsaw Terminal', 'Warsaw Logistics Terminal, Poland', 'WAW001', FALSE, TRUE, TRUE, TRUE, 52.2297, 21.0122, FALSE);
-- Insert validity periods
INSERT INTO validity_period (id, start_date, end_date, state) VALUES
(1, DATE_SUB(NOW(), INTERVAL 1 MONTH), DATE_ADD(NOW(), INTERVAL 11 MONTH), 'VALID'),
(2, DATE_ADD(NOW(), INTERVAL 1 DAY), NULL, 'DRAFT'),
(3, DATE_SUB(NOW(), INTERVAL 2 YEAR), DATE_SUB(NOW(), INTERVAL 1 YEAR), 'EXPIRED'),
(4, DATE_SUB(NOW(), INTERVAL 6 MONTH), DATE_SUB(NOW(), INTERVAL 1 DAY), 'INVALID');
-- Insert container rates for VALID period
INSERT INTO container_rate (id, from_node_id, to_node_id, container_rate_type, rate_teu, rate_feu, rate_hc, lead_time, validity_period_id) VALUES
-- Rail connections
(1, 1, 2, 'RAIL', 1000.00, 1800.00, 2000.00, 3, 1),
(2, 1, 3, 'RAIL', 800.00, 1500.00, 1700.00, 2, 1),
(3, 3, 4, 'RAIL', 1200.00, 2200.00, 2400.00, 4, 1),
(4, 3, 7, 'RAIL', 900.00, 1700.00, 1900.00, 3, 1),
(5, 2, 5, 'RAIL', 1500.00, 2800.00, 3000.00, 5, 1),
-- Sea connections
(6, 1, 6, 'SEA', 2000.00, 3800.00, 4200.00, 14, 1),
(7, 6, 5, 'SEA', 1800.00, 3400.00, 3800.00, 10, 1),
-- Road connections
(8, 2, 3, 'ROAD', 600.00, 1100.00, 1300.00, 1, 1),
(9, 4, 5, 'ROAD', 1400.00, 2600.00, 2900.00, 2, 1),
(10, 7, 8, 'ROAD', 700.00, 1300.00, 1500.00, 1, 1),
-- Post-run connections
(11, 3, 2, 'POST_RUN', 400.00, 750.00, 850.00, 1, 1),
(12, 5, 4, 'POST_RUN', 500.00, 950.00, 1050.00, 1, 1);
-- Insert container rates for DRAFT period
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
(1, 2, 'RAIL', 1100.00, 2000.00, 2200.00, 3, 2),
(1, 3, 'RAIL', 900.00, 1700.00, 1900.00, 2, 2),
(3, 4, 'RAIL', 1300.00, 2400.00, 2600.00, 4, 2);
-- Insert country matrix rates for VALID period
INSERT INTO country_matrix_rate (id, from_country_id, to_country_id, rate, validity_period_id) VALUES
(1, 1, 2, 2.50, 1), -- Germany to France
(2, 1, 3, 3.20, 1), -- Germany to Italy
(3, 1, 4, 3.80, 1), -- Germany to Spain
(4, 1, 5, 2.20, 1), -- Germany to Czech Republic
(5, 2, 3, 2.90, 1), -- France to Italy
(6, 2, 4, 2.40, 1), -- France to Spain
(7, 3, 4, 3.50, 1), -- Italy to Spain
(8, 5, 6, 1.80, 1), -- Czech Republic to Poland
(9, 1, 7, 2.00, 1), -- Germany to Netherlands
(10, 1, 8, 2.30, 1); -- Germany to Belgium
-- Insert country matrix rates for DRAFT period
INSERT INTO country_matrix_rate (from_country_id, to_country_id, rate, validity_period_id) VALUES
(1, 2, 2.70, 2), -- Germany to France (increased rate)
(1, 3, 3.40, 2), -- Germany to Italy (increased rate)
(1, 4, 4.00, 2); -- Germany to Spain (increased rate)
-- Insert property sets for testing
INSERT INTO property_set (id, start_date, end_date, state) VALUES
(1, DATE_SUB(NOW(), INTERVAL 1 MONTH), DATE_ADD(NOW(), INTERVAL 11 MONTH), 'VALID'),
(2, DATE_ADD(NOW(), INTERVAL 1 DAY), NULL, 'DRAFT');
-- Insert system property types
INSERT INTO system_property_type (id, name, external_mapping_id, data_type, validation_rule) VALUES
(1, 'Default Lead Time Buffer', 'LEAD_TIME_BUF', 'INT', 'MIN:0,MAX:30'),
(2, 'Container Loading Factor', 'CONT_LOAD_FAC', 'PERCENTAGE', 'MIN:0,MAX:100'),
(3, 'Enable Rate Approval', 'RATE_APPROVAL', 'BOOLEAN', NULL);
-- Insert system properties
INSERT INTO system_property (property_set_id, system_property_type_id, property_value) VALUES
(1, 1, '2'),
(1, 2, '85'),
(1, 3, 'true');

View file

@ -0,0 +1,23 @@
delete
from sys_user_group_mapping
where 1;
delete
from sys_user_node
where 1;
delete
from sys_group
where 1;
delete
from sys_user
where 1;
ALTER TABLE sys_user_group_mapping AUTO_INCREMENT = 1;
ALTER TABLE sys_user_node AUTO_INCREMENT = 1;
ALTER TABLE sys_group AUTO_INCREMENT = 1;
ALTER TABLE sys_user AUTO_INCREMENT = 1;

View file

@ -0,0 +1,54 @@
-- First, create some fake users since premise table requires user_id
INSERT INTO sys_user (workday_id, email, firstname, lastname, is_active)
VALUES ('USR001', 'john.doe@company.com', 'John', 'Doe', TRUE),
('USR002', 'sarah.smith@company.com', 'Sarah', 'Smith', TRUE),
('USR003', 'mike.johnson@company.com', 'Mike', 'Johnson', TRUE),
('USR004', 'anna.mueller@company.com', 'Anna', 'Mueller', TRUE),
('USR005', 'david.chen@company.com', 'David', 'Chen', TRUE)
ON DUPLICATE KEY UPDATE email = VALUES(email);
INSERT INTO sys_group(group_name, group_description)
VALUES ('default', 'Default user: Can login and generate reports');
INSERT INTO sys_group(group_name, group_description)
VALUES ('LCE', 'Logistic cost expert: Can login, generate reports and do calculations');
INSERT INTO sys_group(group_name, group_description)
VALUES ('freight', 'Freight key user: Can login, generate reports and edit freight rates');
INSERT INTO sys_group(group_name, group_description)
VALUES ('packaging', 'Packaging key user: Can login, generate reports and edit packaging data');
INSERT INTO sys_group(group_name, group_description)
VALUES ('super',
'Super key user: Can login, generate reports, do calculations, edit freight rates, edit packaging data');
INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'),
(SELECT id FROM sys_user WHERE email = 'john.doe@company.com'));
INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'),
(SELECT id FROM sys_user WHERE email = 'sarah.smith@company.com'));
INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'),
(SELECT id FROM sys_user WHERE email = 'mike.johnson@company.com'));
INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'),
(SELECT id FROM sys_user WHERE email = 'anna.mueller@company.com'));
INSERT INTO sys_user_group_mapping (user_id, group_id)
VALUES ((SELECT id FROM sys_group WHERE group_name = 'LCE'),
(SELECT id FROM sys_user WHERE email = 'david.chen@company.com'));
INSERT INTO sys_user_node (user_id, country_id, name, address, geo_lat, geo_lng, is_deprecated)
VALUES ((SELECT id FROM sys_user WHERE email = 'john.doe@company.com'),
(SELECT id FROM country WHERE iso_code = 'CN'),
'My Supplier 1', 'My Road 1, 1234 MyCity',
24.489,
118.1478, false);
INSERT INTO sys_user_node (user_id, country_id, name, address, geo_lat, geo_lng, is_deprecated)
VALUES ((SELECT id FROM sys_user WHERE email = 'sarah.smith@company.com'),
(SELECT id FROM country WHERE iso_code = 'CN'),
'My Supplier 2', 'My Road 2, 1234 MyCity',
24.489,
118.1478, false);

View file

@ -1,561 +0,0 @@
-- DROP TABLE IF EXISTS `lcc`.`calculation_job`, `lcc`.`calculation_job_destination`, `lcc`.`calculation_job_route_section`, `lcc`.`container_rate`, `lcc`.`country`, `lcc`.`country_matrix_rate`, `lcc`.`country_property`, `lcc`.`country_property_type`, `lcc`.`distance_matrix`, `lcc`.`material`, `lcc`.`node`, `lcc`.`node_predecessor_chain`, `lcc`.`node_predecessor_entry`, `lcc`.`outbound_country_mapping`, `lcc`.`packaging`, `lcc`.`packaging_dimension`, `lcc`.`packaging_property`, `lcc`.`packaging_property_type`, `lcc`.`premise`, `lcc`.`premise_destination`, `lcc`.`premise_route`, `lcc`.`premise_route_node`, `lcc`.`premise_route_section`, `lcc`.`property_set`, `lcc`.`sys_group`, `lcc`.`sys_user`, `lcc`.`sys_user_group_mapping`, `lcc`.`sys_user_node`, `lcc`.`system_property`, `lcc`.`system_property_type`, `lcc`.`validity_period`;
-- Property management tables
CREATE TABLE IF NOT EXISTS `property_set`
(
-- Represents a collection of properties valid for a specific time period
`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,
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`),
INDEX `idx_property_set_id` (id)
) COMMENT 'Manages versioned sets of properties with temporal validity';
CREATE TABLE IF NOT EXISTS `system_property_type`
(
-- Stores system-wide configuration property types
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`external_mapping_id` VARCHAR(16),
`data_type` VARCHAR(16) NOT NULL,
`validation_rule` VARCHAR(64),
UNIQUE KEY `idx_external_mapping` (`external_mapping_id`),
CONSTRAINT `chk_system_data_type_values` CHECK (`data_type` IN
('INT', 'PERCENTAGE', 'BOOLEAN', 'CURRENCY', 'ENUMERATION',
'TEXT'))
) COMMENT 'Stores system-wide configuration property types';
CREATE TABLE IF NOT EXISTS `system_property`
(
-- Stores system-wide configuration properties
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`property_set_id` INT NOT NULL,
`system_property_type_id` INT NOT NULL,
`property_value` VARCHAR(500),
FOREIGN KEY (`property_set_id`) REFERENCES `property_set` (`id`),
FOREIGN KEY (`system_property_type_id`) REFERENCES `system_property_type` (`id`),
INDEX `idx_system_property_type_id` (system_property_type_id),
INDEX `idx_property_set_id` (id),
UNIQUE KEY `idx_system_property_type_id_property_set` (`system_property_type_id`, `property_set_id`)
) COMMENT 'Stores system-wide configuration properties';
-- country
CREATE TABLE IF NOT EXISTS `country`
(
`id` INT NOT NULL AUTO_INCREMENT,
`iso_code` CHAR(2) NOT NULL COMMENT 'ISO 3166-1 alpha-2 country code',
`region_code` CHAR(5) NOT NULL COMMENT 'Geographic region code (EMEA/LATAM/APAC/NAM)',
`is_deprecated` BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_country_iso_code` (`iso_code`),
CONSTRAINT `chk_country_region_code`
CHECK (`region_code` IN ('EMEA', 'LATAM', 'APAC', 'NAM'))
) COMMENT 'Master data table for country information and regional classification';
CREATE TABLE IF NOT EXISTS `country_property_type`
(
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(255) NOT NULL,
`external_mapping_id` VARCHAR(16),
`data_type` VARCHAR(16) NOT NULL,
`validation_rule` VARCHAR(64),
`is_required` BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT `chk_country_data_type_values` CHECK (`data_type` IN
('INT', 'PERCENTAGE', 'BOOLEAN', 'CURRENCY', 'ENUMERATION',
'TEXT')),
PRIMARY KEY (`id`),
INDEX `idx_property_type_data_type` (`data_type`)
) COMMENT 'Defines available property types for country-specific configurations';
CREATE TABLE IF NOT EXISTS `country_property`
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`country_id` INT NOT NULL,
`country_property_type_id` INT NOT NULL,
`property_set_id` INT NOT NULL,
`property_value` VARCHAR(500),
FOREIGN KEY (`country_id`) REFERENCES `country` (`id`),
FOREIGN KEY (`country_property_type_id`) REFERENCES `country_property_type` (`id`),
FOREIGN KEY (`property_set_id`) REFERENCES `property_set` (`id`),
UNIQUE KEY `idx_country_property` (`country_id`, `country_property_type_id`, `property_set_id`)
) COMMENT 'Stores country-specific property values with versioning support';
-- Main table for user information
CREATE TABLE IF NOT EXISTS `sys_user`
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`workday_id` CHAR(32) NOT NULL,
`email` VARCHAR(254) NOT NULL,
`firstname` VARCHAR(100) NOT NULL,
`lastname` VARCHAR(100) NOT NULL,
`is_active` BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE KEY `idx_user_email` (`email`),
UNIQUE KEY `idx_user_workday` (`workday_id`)
) COMMENT 'Stores basic information about system users';
-- Group definitions
CREATE TABLE IF NOT EXISTS `sys_group`
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`group_name` VARCHAR(64) NOT NULL,
`group_description` VARCHAR(128) NOT NULL,
UNIQUE KEY `idx_group_name` (`group_name`)
) COMMENT 'Defines user groups for access management';
-- Junction table for user-group assignments
CREATE TABLE IF NOT EXISTS `sys_user_group_mapping`
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`group_id` INT NOT NULL,
UNIQUE INDEX `idx_user_group` (`user_id`, `group_id`),
FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`),
FOREIGN KEY (`group_id`) REFERENCES `sys_group` (`id`)
) COMMENT 'Links users with their associated groups';
CREATE TABLE IF NOT EXISTS `sys_user_node`
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`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),
`is_deprecated` BOOLEAN DEFAULT FALSE,
FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`),
FOREIGN KEY (`country_id`) REFERENCES `country` (`id`)
) COMMENT 'Contains user generated logistic nodes';
-- logistic nodes
CREATE TABLE IF NOT EXISTS node
(
id INT PRIMARY KEY,
country_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
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),
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),
INDEX idx_country_id (country_id)
) COMMENT '';
CREATE TABLE IF NOT EXISTS node_predecessor_chain
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
node_id INT NOT NULL,
FOREIGN KEY (node_id) REFERENCES node (id)
);
CREATE TABLE IF NOT EXISTS node_predecessor_entry
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
node_id INT NOT NULL,
node_predecessor_chain_id INT NOT NULL,
sequence_number INT NOT NULL CHECK (sequence_number > 0),
FOREIGN KEY (node_id) REFERENCES node (id),
FOREIGN KEY (node_predecessor_chain_id) REFERENCES node_predecessor_chain (id),
UNIQUE KEY uk_node_predecessor (node_predecessor_chain_id, sequence_number),
INDEX idx_node_predecessor (node_predecessor_chain_id),
INDEX idx_sequence (sequence_number)
);
CREATE TABLE IF NOT EXISTS outbound_country_mapping
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
node_id INT NOT NULL,
country_id INT NOT NULL,
FOREIGN KEY (node_id) REFERENCES node (id),
FOREIGN KEY (country_id) REFERENCES country (id),
UNIQUE KEY uk_node_id_country_id (node_id, country_id),
INDEX idx_node_id (node_id),
INDEX idx_country_id (country_id)
);
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),
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,
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')),
INDEX idx_from_to_nodes (from_node_id, to_node_id)
);
-- container rates
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')),
CONSTRAINT `chk_validity_date_range` CHECK (`end_date` IS NULL OR `end_date` > `start_date`)
);
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')),
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',
lead_time INT UNSIGNED NOT NULL COMMENT 'lead time in days',
validity_period_id INT NOT NULL,
FOREIGN KEY (from_node_id) REFERENCES node (id),
FOREIGN KEY (to_node_id) REFERENCES node (id),
FOREIGN KEY (validity_period_id) REFERENCES validity_period (id),
INDEX idx_from_to_nodes (from_node_id, to_node_id),
INDEX idx_validity_period_id (validity_period_id)
);
CREATE TABLE IF NOT EXISTS country_matrix_rate
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
from_country_id INT NOT NULL,
to_country_id INT NOT NULL,
rate DECIMAL(15, 2) NOT NULL COMMENT 'rate for full truck load per kilometer in EUR',
validity_period_id INT NOT NULL,
FOREIGN KEY (from_country_id) REFERENCES country (id),
FOREIGN KEY (to_country_id) REFERENCES country (id),
FOREIGN KEY (validity_period_id) REFERENCES validity_period (id),
INDEX idx_from_to_country (from_country_id, to_country_id),
INDEX idx_validity_period_id (validity_period_id)
);
-- packaging and material
CREATE TABLE IF NOT EXISTS material
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
part_number CHAR(12) NOT NULL,
normalized_part_number CHAR(12) NOT NULL,
hs_code CHAR(8),
name VARCHAR(500) NOT NULL,
is_deprecated BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT `uq_normalized_part_number` UNIQUE (`normalized_part_number`)
);
CREATE TABLE IF NOT EXISTS packaging_dimension
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`type` CHAR(3) DEFAULT 'HU',
`length` INT UNSIGNED NOT NULL COMMENT 'length stored in mm',
`width` INT UNSIGNED NOT NULL COMMENT 'width stored in mm',
`height` INT UNSIGNED NOT NULL COMMENT 'height stored in mm',
`displayed_dimension_unit` CHAR(2) DEFAULT 'CM',
`weight` INT UNSIGNED NOT NULL COMMENT 'weight stored in g',
`displayed_weight_unit` CHAR(2) DEFAULT 'KG',
`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')),
CONSTRAINT `chk_packaging_dimension_displayed_dimension_unit` CHECK (`displayed_dimension_unit` IN
('MM', 'CM', 'M')),
CONSTRAINT `chk_packaging_dimension_displayed_weight_unit` CHECK (`displayed_weight_unit` IN
('G', 'KG'))
);
CREATE TABLE IF NOT EXISTS packaging
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`supplier_node_id` INT NOT NULL,
`material_id` INT NOT NULL,
`hu_dimension_id` INT NOT NULL,
`shu_dimension_id` INT NOT NULL,
`is_deprecated` BOOLEAN NOT NULL DEFAULT FALSE,
FOREIGN KEY (supplier_node_id) REFERENCES node (id),
FOREIGN KEY (material_id) REFERENCES material (id),
FOREIGN KEY (hu_dimension_id) REFERENCES packaging_dimension (id),
FOREIGN KEY (shu_dimension_id) REFERENCES packaging_dimension (id),
INDEX idx_material_id (material_id),
INDEX idx_hu_dimension_id (hu_dimension_id),
INDEX idx_shu_dimension_id (shu_dimension_id)
);
CREATE TABLE IF NOT EXISTS packaging_property_type
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
external_mapping_id VARCHAR(16) NOT NULL,
`data_type` VARCHAR(16),
`validation_rule` VARCHAR(64),
`is_required` BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE KEY idx_packaging_property_type (`external_mapping_id`),
CONSTRAINT `chk_packaging_data_type_values` CHECK (`data_type` IN
('INT', 'PERCENTAGE', 'BOOLEAN', 'CURRENCY', 'ENUMERATION',
'TEXT'))
);
CREATE TABLE IF NOT EXISTS packaging_property
(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`packaging_property_type_id` INT NOT NULL,
`packaging_id` INT NOT NULL,
`property_value` VARCHAR(500),
FOREIGN KEY (packaging_property_type_id) REFERENCES packaging_property_type (id),
FOREIGN KEY (packaging_id) REFERENCES packaging (id),
INDEX idx_packaging_property_type_id (packaging_property_type_id),
INDEX idx_packaging_id (packaging_id),
UNIQUE KEY idx_packaging_property_unique (packaging_property_type_id, packaging_id)
);
CREATE TABLE IF NOT EXISTS premise
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
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),
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',
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',
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)',
individual_hu_weight INT UNSIGNED COMMENT 'user entered weight in g (if system-wide packaging is used, packaging weight are copied here after creation)',
hu_displayed_dimension_unit CHAR(2) DEFAULT 'MM',
hu_displayed_weight_unit CHAR(2) DEFAULT 'G',
hu_unit_count INT UNSIGNED DEFAULT NULL,
hu_stackable BOOLEAN DEFAULT TRUE,
hu_mixable BOOLEAN DEFAULT TRUE,
FOREIGN KEY (material_id) REFERENCES material (id),
FOREIGN KEY (supplier_node_id) REFERENCES node (id),
FOREIGN KEY (user_supplier_node_id) REFERENCES sys_user_node (id),
FOREIGN KEY (packaging_id) REFERENCES packaging (id),
FOREIGN KEY (user_id) REFERENCES sys_user (id),
CONSTRAINT `chk_premise_state_values` CHECK (`state` IN
('DRAFT', 'COMPLETED', 'ARCHIVED', 'DELETED')),
CONSTRAINT `chk_premise_displayed_dimension_unit` CHECK (`hu_displayed_dimension_unit` IN
('MM', 'CM', 'M')),
CONSTRAINT `chk_premise_displayed_weight_unit` CHECK (`hu_displayed_weight_unit` IN
('G', 'KG')),
INDEX idx_material_id (material_id),
INDEX idx_supplier_node_id (supplier_node_id),
INDEX idx_packaging_id (packaging_id),
INDEX idx_user_id (user_id),
INDEX idx_user_supplier_node_id (user_supplier_node_id)
);
CREATE TABLE IF NOT EXISTS premise_destination
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
premise_id INT NOT NULL,
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),
country_id INT NOT NULL,
FOREIGN KEY (premise_id) REFERENCES premise (id),
FOREIGN KEY (country_id) REFERENCES country (id),
FOREIGN KEY (destination_node_id) REFERENCES node (id),
INDEX idx_destination_node_id (destination_node_id),
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,
node_id INT DEFAULT NULL,
user_node_id INT DEFAULT NULL,
name VARCHAR(255) NOT NULL,
address VARCHAR(500),
country_id INT NOT NULL,
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),
is_outdated BOOLEAN DEFAULT FALSE,
FOREIGN KEY (node_id) REFERENCES node (id),
FOREIGN KEY (country_id) REFERENCES country (id),
FOREIGN KEY (user_node_id) REFERENCES sys_user_node (id),
INDEX idx_node_id (node_id),
INDEX idx_user_node_id (user_node_id),
CONSTRAINT `chk_node` CHECK (`user_node_id` IS NULL OR `node_id` IS NULL)
);
CREATE TABLE IF NOT EXISTS premise_route_section
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
premise_route_id INT NOT NULL,
from_route_node_id INT NOT NULL,
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')),
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),
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),
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,
premise_id INT NOT NULL,
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')),
user_id INT NOT NULL,
FOREIGN KEY (premise_id) REFERENCES premise (id),
FOREIGN KEY (validity_period_id) REFERENCES validity_period (id),
FOREIGN KEY (property_set_id) REFERENCES property_set (id),
FOREIGN KEY (user_id) REFERENCES sys_user (id),
INDEX idx_premise_id (premise_id),
INDEX idx_validity_period_id (validity_period_id),
INDEX idx_property_set_id (property_set_id)
);
CREATE TABLE IF NOT EXISTS calculation_job_destination
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
calculation_job_id INT NOT NULL,
premise_destination_id INT NOT NULL,
shipping_frequency INT UNSIGNED COMMENT 'annual shipping frequency',
total_cost DECIMAL(15, 2) COMMENT 'aka MEK_B in EUR (excl. Airfreight)',
annual_amount DECIMAL(15, 2) COMMENT 'annual quantity for this destinations in pieces',
-- risk
annual_risk_cost DECIMAL(15, 2) NOT NULL COMMENT 'complete calculation with globally stored worst case container rates (excl. Airfreight)',
annual_chance_cost DECIMAL(15, 2) NOT NULL COMMENT 'complete calculation with globally stored best case container rates (excl. Airfreight)',
-- handling
is_small_unit BOOLEAN DEFAULT FALSE COMMENT 'small unit equals KLT, volume of a handling unit is smaller than 0.08 cbm ',
annual_repacking_cost DECIMAL(15, 2) NOT NULL,
annual_handling_cost DECIMAL(15, 2) NOT NULL,
annual_disposal_cost DECIMAL(15, 2) NOT NULL,
-- inventory
operational_stock DECIMAL(15, 2) NOT NULL COMMENT 'operational stock in single pieces',
safety_stock DECIMAL(15, 2) NOT NULL COMMENT 'safety stock in single pieces',
stocked_inventory DECIMAL(15, 2) NOT NULL COMMENT 'sum of operational and safety stock',
in_transport_stock DECIMAL(15, 2) NOT NULL,
stock_before_payment DECIMAL(15, 2) NOT NULL,
annual_capital_cost DECIMAL(15, 2) NOT NULL,
annual_storage_cost DECIMAL(15, 2) NOT NULL, -- Flächenkosten
-- 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,
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_volumetric_weight DECIMAL(15, 2) NOT NULL,
air_freight_weight DECIMAL(15, 2) NOT NULL,
annual_air_freight_cost DECIMAL(15, 2) NOT NULL,
-- transportation
is_d2d BOOLEAN DEFAULT FALSE,
rate_d2d DECIMAL(15, 2) DEFAULT NULL,
container_type CHAR(8) CHECK (container_type IN
('TEU', 'FEU', 'HC', 'TRUCK')),
hu_count INT UNSIGNED NOT NULL COMMENT 'number of handling units int total',
layer_structure JSON COMMENT 'json representation of a single layer',
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,
transit_time_in_days INT UNSIGNED NOT NULL,
safety_stock_in_days INT UNSIGNED NOT NULL,
-- material cost
material_cost DECIMAL(15, 2) NOT NULL,
fca_cost DECIMAL(15, 2) NOT NULL,
FOREIGN KEY (calculation_job_id) REFERENCES calculation_job (id),
FOREIGN KEY (premise_destination_id) REFERENCES premise_destination (id),
INDEX idx_calculation_job_id (calculation_job_id),
INDEX idx_premise_destination_id (premise_destination_id)
);
CREATE TABLE IF NOT EXISTS calculation_job_route_section
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
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')),
is_unmixed_price BOOLEAN DEFAULT FALSE,
is_cbm_price BOOLEAN DEFAULT FALSE,
is_weight_price BOOLEAN DEFAULT FALSE,
is_stacked BOOLEAN DEFAULT FALSE,
is_pre_run BOOLEAN DEFAULT FALSE,
is_main_run BOOLEAN DEFAULT FALSE,
is_post_run BOOLEAN DEFAULT FALSE,
rate DECIMAL(15, 2) NOT NULL COMMENT 'copy of the container rate resp. price matrix in EUR (depends on used_rule)',
distance DECIMAL(15, 2) DEFAULT NULL COMMENT 'distance of this routeInformationObject section im meters',
cbm_price DECIMAL(15, 2) NOT NULL COMMENT 'calculated price per cubic meter',
weight_price DECIMAL(15, 2) NOT NULL COMMENT 'calculated price per kilogram',
annual_cost DECIMAL(15, 2) NOT NULL COMMENT 'annual costs for this routeInformationObject section, result depends on calculation method (mixed or unmixed, stacked or unstacked, per volume/per weight resp. container rate/price matrix)',
transit_time INT UNSIGNED NOT NULL,
FOREIGN KEY (premise_route_section_id) REFERENCES premise_route_section (id),
FOREIGN KEY (calculation_job_destination_id) REFERENCES calculation_job_destination (id),
INDEX idx_premise_route_section_id (premise_route_section_id),
INDEX idx_calculation_job_destination_id (calculation_job_destination_id),
CONSTRAINT chk_stacked CHECK (is_unmixed_price IS TRUE OR is_stacked IS TRUE), -- only unmixed transports can be unstacked
CONSTRAINT chk_cbm_weight_price CHECK (is_unmixed_price IS FALSE OR
(is_cbm_price IS FALSE AND is_weight_price IS FALSE))
);