diff --git a/src/main/java/de/avatic/lcc/controller/calculation/CalculationController.java b/src/main/java/de/avatic/lcc/controller/calculation/CalculationController.java index 447ff56..921fdc2 100644 --- a/src/main/java/de/avatic/lcc/controller/calculation/CalculationController.java +++ b/src/main/java/de/avatic/lcc/controller/calculation/CalculationController.java @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.List; +import java.util.Map; @RestController @RequestMapping("/api/calculation") @@ -70,7 +71,7 @@ public class CalculationController { // triggers a calculation @PutMapping("/done") public ResponseEntity completePremises(@RequestBody List premiseIds) { - return ResponseEntity.ok(premisesServices.startCalculation(premiseIds)); + return ResponseEntity.ok(premisesServices.startCalculation(premiseIds)); //TODO } @PutMapping("/packaging") @@ -89,7 +90,7 @@ public class CalculationController { } @PostMapping("/destination") - public ResponseEntity> createDestination(@RequestBody DestinationCreateDTO destinationCreateDTO) { + public ResponseEntity> createDestination(@RequestBody DestinationCreateDTO destinationCreateDTO) { return ResponseEntity.ok(destinationService.createDestination(destinationCreateDTO)); } diff --git a/src/main/java/de/avatic/lcc/dto/calculation/edit/SetDataDTO.java b/src/main/java/de/avatic/lcc/dto/calculation/edit/SetDataDTO.java index 5c2e610..ce872eb 100644 --- a/src/main/java/de/avatic/lcc/dto/calculation/edit/SetDataDTO.java +++ b/src/main/java/de/avatic/lcc/dto/calculation/edit/SetDataDTO.java @@ -12,7 +12,7 @@ public class SetDataDTO { @JsonProperty("update_master_data") boolean updateMasterData; - @JsonProperty("supplier_node_id", required = false) + @JsonProperty(defaultValue = "supplier_node_id", required = false) Integer supplierNodeId; @JsonProperty("is_user_supplier_node") diff --git a/src/main/java/de/avatic/lcc/model/premises/route/RouteInformation.java b/src/main/java/de/avatic/lcc/model/premises/route/RouteInformation.java index cd6685e..1a2d157 100644 --- a/src/main/java/de/avatic/lcc/model/premises/route/RouteInformation.java +++ b/src/main/java/de/avatic/lcc/model/premises/route/RouteInformation.java @@ -7,20 +7,22 @@ public class RouteInformation { private Route route; - private List sections; + private List sections; - private List nodes; + public Route getRoute() { + return route; + } public void setRoute(Route route) { this.route = route; } - public void setRouteNodes(List nodes) { - this.nodes = nodes; + public List getSections() { + return sections; } - public void setRouteSections(List sections) { + public void setSections(List sections) { this.sections = sections; } } diff --git a/src/main/java/de/avatic/lcc/model/premises/route/RouteSectionInformation.java b/src/main/java/de/avatic/lcc/model/premises/route/RouteSectionInformation.java new file mode 100644 index 0000000..7f8060c --- /dev/null +++ b/src/main/java/de/avatic/lcc/model/premises/route/RouteSectionInformation.java @@ -0,0 +1,39 @@ +package de.avatic.lcc.model.premises.route; + +public class RouteSectionInformation { + + private RouteNode fromNode; + private RouteNode toNode; + private RouteSection section; + + public RouteSectionInformation(RouteSection routeSection, RouteNode fromNode, RouteNode toNode) { + this.section = routeSection; + this.fromNode = fromNode; + this.toNode = toNode; + } + + + public RouteNode getFromNode() { + return fromNode; + } + + public void setFromNode(RouteNode fromNode) { + this.fromNode = fromNode; + } + + public RouteNode getToNode() { + return toNode; + } + + public void setToNode(RouteNode toNode) { + this.toNode = toNode; + } + + public RouteSection getSection() { + return section; + } + + public void setSection(RouteSection section) { + this.section = section; + } +} diff --git a/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java b/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java index 7d9939b..740e12a 100644 --- a/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/MaterialRepository.java @@ -15,9 +15,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; @Repository public class MaterialRepository { @@ -29,28 +27,6 @@ public class MaterialRepository { this.jdbcTemplate = jdbcTemplate; } - public List listAllMaterials() { - String query = "SELECT * FROM material ORDER BY normalized_part_number"; - return jdbcTemplate.query(query, new MaterialMapper()); - } - - - private static class MaterialMapper implements RowMapper { - - @Override - public Material mapRow(ResultSet rs, int rowNum) throws SQLException { - Material data = new Material(); - data.setId(rs.getInt("id")); - data.setName(rs.getString("name")); - data.setPartNumber(rs.getString("part_number")); - data.setNormalizedPartNumber(rs.getString("normalized_part_number")); - data.setHsCode(rs.getString("hs_code")); - data.setDeprecated(rs.getBoolean("is_deprecated")); - return data; - } - } - - private static String buildCountQuery(String filter, boolean excludeDeprecated) { StringBuilder queryBuilder = new StringBuilder(""" SELECT count(*) @@ -66,7 +42,6 @@ public class MaterialRepository { return queryBuilder.toString(); } - private static String buildQuery(String filter, boolean excludeDeprecated) { StringBuilder queryBuilder = new StringBuilder(""" SELECT id, name, part_number, normalized_part_number, hs_code, is_deprecated @@ -82,8 +57,25 @@ public class MaterialRepository { return queryBuilder.toString(); } + public List listAllMaterials() { + String query = "SELECT * FROM material ORDER BY normalized_part_number"; + return jdbcTemplate.query(query, new MaterialMapper()); + } + + public List getByPartNumbers(Collection partNumbers) { + // Handle null or empty case + if (partNumbers == null || partNumbers.isEmpty()) { + return Collections.emptyList(); + } + + String placeholders = String.join(",", Collections.nCopies(partNumbers.size(), "?")); + String query = "SELECT * FROM material WHERE part_number IN (" + placeholders + ")"; + + return jdbcTemplate.query(query, new MaterialMapper(), partNumbers.toArray()); + } + @Transactional - public Optional setDeprecatedById(Integer id) { + public Optional setDeprecatedById(Integer id) { String query = "UPDATE material SET is_deprecated = TRUE WHERE id = ?"; return Optional.ofNullable(jdbcTemplate.update(query, id) == 0 ? null : id); } @@ -149,4 +141,19 @@ public class MaterialRepository { } + private static class MaterialMapper implements RowMapper { + + @Override + public Material mapRow(ResultSet rs, int rowNum) throws SQLException { + Material data = new Material(); + data.setId(rs.getInt("id")); + data.setName(rs.getString("name")); + data.setPartNumber(rs.getString("part_number")); + data.setNormalizedPartNumber(rs.getString("normalized_part_number")); + data.setHsCode(rs.getString("hs_code")); + data.setDeprecated(rs.getBoolean("is_deprecated")); + return data; + } + } + } diff --git a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java index 6a6405e..c9fe46b 100644 --- a/src/main/java/de/avatic/lcc/repositories/NodeRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/NodeRepository.java @@ -1,5 +1,6 @@ package de.avatic.lcc.repositories; +import de.avatic.lcc.dto.generic.NodeDTO; import de.avatic.lcc.dto.generic.NodeType; import de.avatic.lcc.model.nodes.Node; import de.avatic.lcc.repositories.pagination.SearchQueryPagination; @@ -32,9 +33,21 @@ public class NodeRepository { FROM node WHERE node.id = ?"""; - var chain = jdbcTemplate.queryForObject(query, new NodeMapper(), id); + var node = jdbcTemplate.queryForObject(query, new NodeMapper(), id); - return Optional.ofNullable(chain); + return Optional.ofNullable(node); + } + + public List getByIds(List nodeIds) { + + String placeholders = String.join(",", Collections.nCopies(nodeIds.size(), "?")); + String query = """ + SELECT node.id AS id, node.name AS name, node.address as address, node.is_source as is_source, + node.is_destination as is_destination, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required + FROM node + WHERE node.id IN (?)"""; + + return jdbcTemplate.query(query, new NodeMapper(), nodeIds.toArray()); } private List> getPredecessorsOf(Integer id) { @@ -281,6 +294,7 @@ public class NodeRepository { return jdbcTemplate.query(query, new NodeMapper(), countryId); } + private class NodeMapper implements RowMapper { @Override diff --git a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java index a921c80..40bb6b7 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java @@ -3,11 +3,14 @@ package de.avatic.lcc.repositories.premise; import de.avatic.lcc.model.premises.route.Destination; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Service; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -70,6 +73,38 @@ public class DestinationRepository { return Optional.of(userId.getFirst()); } + public List getByPremiseIdsAndNodeId(List premiseId, Integer nodeId, Integer userId) { + String placeholder = String.join(",", Collections.nCopies(premiseId.size(), "?")); + String query = "SELECT * FROM premise_destination JOIN premise ON premise_destination.premise_id = premise.id WHERE premise_id IN ("+placeholder+") AND premise_destination.destination_node_id AND premise.user_id = ?"; + + return jdbcTemplate.query(query, new DestinationMapper(), premiseId, nodeId, userId); + } + + public Integer insert(Destination destination) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + + String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, rate_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.update(connection -> { + var ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); + ps.setBigDecimal(1, destination.getAnnualAmount()); + ps.setInt(2, destination.getPremiseId()); + ps.setInt(3, destination.getDestinationNodeId()); + ps.setBigDecimal(4, destination.getRateD2d()); + ps.setBoolean(5, destination.getD2d()); + ps.setBigDecimal(6, destination.getRepackingCost()); + ps.setBigDecimal(7, destination.getHandlingCost()); + ps.setBigDecimal(8, destination.getDisposalCost()); + return ps; + }, keyHolder); + + return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null; + } + + public void duplicate(Integer fromId, Integer toId) { + + } + private static class DestinationMapper implements RowMapper { @Override diff --git a/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java index d006daf..9c4e8a0 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/PremiseRepository.java @@ -13,12 +13,17 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -34,6 +39,7 @@ public class PremiseRepository { this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; } + @Transactional public SearchQueryResult listPremises(String filter, SearchQueryPagination pagination, Integer userId, Boolean deleted, Boolean archived, Boolean done) { @@ -111,6 +117,7 @@ public class PremiseRepository { } + @Transactional(readOnly = true) public List getPremisesById(List premiseIds) { @@ -132,6 +139,7 @@ public class PremiseRepository { ); } + @Transactional public Optional getPremiseById(Integer id) { String query = """ @@ -189,6 +197,7 @@ public class PremiseRepository { } + @Transactional public void updateMaterial(List premiseIds, Integer userId, String hsCode, BigDecimal tariffRate) { String placeholders = String.join(",", Collections.nCopies(premiseIds.size(), "?")); @@ -214,6 +223,7 @@ public class PremiseRepository { ); } + @Transactional public void updatePrice(List premiseIds, int userId, BigDecimal price, Boolean includeFcaFee, BigDecimal overseaShare) { String placeholders = String.join(",", Collections.nCopies(premiseIds.size(), "?")); @@ -255,6 +265,7 @@ public class PremiseRepository { * @return A list of premises in the DRAFT state with the same material and supplier combination (excluding the specified premise). * @throws IllegalArgumentException If any of the provided parameters are null. */ + @Transactional(readOnly = true) public List getCollidingPremisesOnChange(Integer userId, Integer premiseId, Integer materialId, Integer supplierId) { if( premiseId == null || materialId == null || supplierId == null ) @@ -270,6 +281,7 @@ public class PremiseRepository { return jdbcTemplate.query(sql, new PremiseMapper(), materialId, supplierId, premiseId, userId); } + @Transactional public void deletePremisesById(List premiseIds) { if(premiseIds == null || premiseIds.isEmpty()) return; @@ -281,12 +293,14 @@ public class PremiseRepository { jdbcTemplate.update(query, premiseIds.toArray()); } + @Transactional public void updateTariffRate(Integer premiseId, Number tariffRate) { String sql = "UPDATE premise SET tariff_rate = ? WHERE id = ?"; jdbcTemplate.update(sql, tariffRate, premiseId); } + @Transactional public void setSupplierId(List premiseId, Integer supplierId, boolean userSupplierNode) { String placeholders = String.join(",", Collections.nCopies(premiseId.size(), "?")); String sql = "UPDATE premise SET supplier_node_id = ?, user_supplier_node_id = ? WHERE id IN ("+placeholders+")"; @@ -298,6 +312,124 @@ public class PremiseRepository { } } + @Transactional + public void setMaterialId(List premiseId, Integer materialId) { + String placeholders = String.join(",", Collections.nCopies(premiseId.size(), "?")); + String sql = "UPDATE premise SET material_id = ? WHERE id IN ("+placeholders+")"; + + jdbcTemplate.update(sql, materialId, premiseId.toArray()); + } + + @Transactional + public List getPremisesByMaterialIdsAndSupplierIds(List materialIds, List supplierIds, List userSupplierIds, Integer userId, boolean draftsOnly) { + + if ((materialIds == null || materialIds.isEmpty()) && + (supplierIds == null || supplierIds.isEmpty()) && + (userSupplierIds == null || userSupplierIds.isEmpty())) { + return Collections.emptyList(); + } + + StringBuilder query = new StringBuilder(""" + SELECT * FROM premise + WHERE 1=1 + """); + + if(draftsOnly) + query.append(" AND user_id = ? AND state = ?"); + + // Append conditions for IDs + if (materialIds != null && !materialIds.isEmpty()) { + String materialPlaceholders = String.join(",", Collections.nCopies(materialIds.size(), "?")); + query.append(" AND material_id IN (").append(materialPlaceholders).append(")"); + } + + if (supplierIds != null && !supplierIds.isEmpty()) { + String supplierPlaceholders = String.join(",", Collections.nCopies(supplierIds.size(), "?")); + query.append(" AND supplier_node_id IN (").append(supplierPlaceholders).append(")"); + } + + if (userSupplierIds != null && !userSupplierIds.isEmpty()) { + String userSupplierPlaceholders = String.join(",", Collections.nCopies(userSupplierIds.size(), "?")); + query.append(" AND user_supplier_node_id IN (").append(userSupplierPlaceholders).append(")"); + } + + // Combine parameters for prepared statement + var params = new ArrayList<>(); + + if(draftsOnly) { + params.add(userId); + params.add(PremiseState.DRAFT.name()); + } + + if (materialIds != null && !materialIds.isEmpty()) params.addAll(materialIds); + if (supplierIds != null && !supplierIds.isEmpty()) params.addAll(supplierIds); + if (userSupplierIds != null && !userSupplierIds.isEmpty()) params.addAll(userSupplierIds); + + return jdbcTemplate.query(query.toString(), new PremiseMapper(), params.toArray()); + } + + @Transactional + public Integer insert(Integer materialId, Integer supplierId, Integer userSupplierId, 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, createdAt, updatedAt)" + + " 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); + + return ps; + }, keyHolder); + + return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null; + + } + + @Transactional + public List findAssociatedSuppliers(List materialIds) { + if (materialIds == null || materialIds.isEmpty()) { + return Collections.emptyList(); + } + + String placeholders = String.join(",", Collections.nCopies(materialIds.size(), "?")); + String query = """ + SELECT DISTINCT supplier_node_id + FROM premise + WHERE material_id IN (""" + placeholders + ")"; + + return jdbcTemplate.query( + query, + (rs, rowNum) -> rs.getInt("supplier_node_id"), + materialIds.toArray() + ); + } + + @Transactional + public List findAssociatedUserSuppliers(List materialIds) { + + if (materialIds == null || materialIds.isEmpty()) { + return Collections.emptyList(); + } + + String placeholders = String.join(",", Collections.nCopies(materialIds.size(), "?")); + String query = """ + SELECT DISTINCT user_supplier_node_id + FROM premise + WHERE material_id IN (""" + placeholders + ")"; + + return jdbcTemplate.query( + query, + (rs, rowNum) -> rs.getInt("user_supplier_node_id"), + materialIds.toArray() + ); + } + /** * Encapsulates SQL query building logic */ diff --git a/src/main/java/de/avatic/lcc/repositories/premise/RouteNodeRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/RouteNodeRepository.java index 9f2ac89..c690b5f 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/RouteNodeRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/RouteNodeRepository.java @@ -3,10 +3,14 @@ package de.avatic.lcc.repositories.premise; import de.avatic.lcc.model.premises.route.RouteNode; 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.stereotype.Repository; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -43,6 +47,29 @@ public class RouteNodeRepository { jdbcTemplate.update(sql, ids.toArray(new Object[0])); } + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, node.getName()); + ps.setString(2, node.getAddress()); + ps.setBigDecimal(3, node.getGeoLat()); + ps.setBigDecimal(4, node.getGeoLng()); + ps.setBoolean(5, Boolean.TRUE.equals(node.getDestination())); + ps.setBoolean(6, Boolean.TRUE.equals(node.getIntermediate())); + ps.setBoolean(7, Boolean.TRUE.equals(node.getSource())); + 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())); + return ps; + }, keyHolder); + + return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null; + } + private static class RouteNodeMapper implements RowMapper { @Override diff --git a/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java index fe8f5fd..595c6d6 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/RouteRepository.java @@ -3,12 +3,17 @@ package de.avatic.lcc.repositories.premise; import de.avatic.lcc.model.premises.route.Route; 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.stereotype.Repository; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.Collections; import java.util.List; +import java.util.Objects; @Repository public class RouteRepository { @@ -36,6 +41,26 @@ public class RouteRepository { jdbcTemplate.update(sql, ids.toArray(new Object[0])); } + public Integer insert(Route premiseRoute) { + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement( + "INSERT INTO premise_route (is_cheapest, is_fastest, is_selected, premise_destination_id) " + + "VALUES (?, ?, ?, ?)", + Statement.RETURN_GENERATED_KEYS); + + ps.setBoolean(1, premiseRoute.getCheapest()); + ps.setBoolean(2, premiseRoute.getFastest()); + ps.setBoolean(3, premiseRoute.getSelected()); + ps.setInt(4, premiseRoute.getDestinationId()); + + return ps; + }, keyHolder); + + return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null; + } + private static class RouteMapper implements RowMapper { @Override public Route mapRow(ResultSet rs, int rowNum) throws SQLException { diff --git a/src/main/java/de/avatic/lcc/repositories/premise/RouteSectionRepository.java b/src/main/java/de/avatic/lcc/repositories/premise/RouteSectionRepository.java index 91d1c4f..c489289 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/RouteSectionRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/RouteSectionRepository.java @@ -4,10 +4,14 @@ import de.avatic.lcc.dto.generic.RouteType; import de.avatic.lcc.model.premises.route.RouteSection; 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.stereotype.Repository; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.util.Collections; import java.util.List; @@ -38,6 +42,28 @@ public class RouteSectionRepository { jdbcTemplate.update(sql, ids.toArray(new Object[0])); } + public Integer insert(RouteSection premiseRouteSection) { + String sql = "INSERT INTO premise_route_section (premise_route_id, from_route_node_id, to_route_node_id, list_position, transport_type, is_pre_run, is_main_run, is_post_run, is_outdated) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setInt(1, premiseRouteSection.getRouteId()); + ps.setInt(2, premiseRouteSection.getFromRouteNodeId()); + ps.setInt(3, premiseRouteSection.getToRouteNodeId()); + ps.setInt(4, premiseRouteSection.getListPosition()); + ps.setString(5, premiseRouteSection.getTransportType().name()); + ps.setBoolean(6, premiseRouteSection.getPreRun()); + ps.setBoolean(7, premiseRouteSection.getMainRun()); + ps.setBoolean(8, premiseRouteSection.getPostRun()); + ps.setBoolean(9, premiseRouteSection.getOutdated()); + return ps; + }, keyHolder); + + return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null; + } + private static class RouteSectionMapper implements RowMapper { @Override public RouteSection mapRow(ResultSet rs, int rowNum) throws SQLException { diff --git a/src/main/java/de/avatic/lcc/service/access/DestinationService.java b/src/main/java/de/avatic/lcc/service/access/DestinationService.java index 11b31c2..513d84c 100644 --- a/src/main/java/de/avatic/lcc/service/access/DestinationService.java +++ b/src/main/java/de/avatic/lcc/service/access/DestinationService.java @@ -3,20 +3,20 @@ package de.avatic.lcc.service.access; 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.model.premises.route.Route; -import de.avatic.lcc.model.premises.route.RouteSection; -import de.avatic.lcc.repositories.premise.DestinationRepository; -import de.avatic.lcc.repositories.premise.RouteNodeRepository; -import de.avatic.lcc.repositories.premise.RouteRepository; -import de.avatic.lcc.repositories.premise.RouteSectionRepository; +import de.avatic.lcc.model.nodes.Node; +import de.avatic.lcc.model.premises.route.*; +import de.avatic.lcc.repositories.NodeRepository; +import de.avatic.lcc.repositories.premise.*; +import de.avatic.lcc.repositories.users.UserNodeRepository; +import de.avatic.lcc.service.calculation.RoutingService; import de.avatic.lcc.service.transformer.premise.DestinationTransformer; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; +import java.math.BigDecimal; +import java.sql.Array; +import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; @Service @@ -28,25 +28,62 @@ public class DestinationService { private final RouteRepository routeRepository; private final RouteSectionRepository routeSectionRepository; private final RouteNodeRepository routeNodeRepository; + private final RoutingService routingService;; + private final NodeRepository nodeRepository; + private final PremiseRepository premiseRepository; + private final UserNodeRepository userNodeRepository; - public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository) { + public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository) { this.destinationRepository = destinationRepository; this.destinationTransformer = destinationTransformer; this.routeRepository = routeRepository; this.routeSectionRepository = routeSectionRepository; this.routeNodeRepository = routeNodeRepository; + this.routingService = routingService; + this.nodeRepository = nodeRepository; + this.premiseRepository = premiseRepository; + this.userNodeRepository = userNodeRepository; } - public HashMap createDestination(DestinationCreateDTO destinationCreateDTO) { + @Transactional + public Map createDestination(DestinationCreateDTO dto) { - // do some checks - // - no duplicates + Integer userId = 1; //TODO get user id. - // do routing. + var existingDestinations = destinationRepository.getByPremiseIdsAndNodeId(dto.getPremiseId(), dto.getDestinationNodeId(), userId); - // create database entries. + var premisesIdsToProcess = new ArrayList(); + for(var premiseId : dto.getPremiseId()) { + if(existingDestinations.stream().map(Destination::getPremiseId).noneMatch(id -> id.equals(premiseId))) { + premisesIdsToProcess.add(premiseId); + } + } + + var premisesToProcess = premiseRepository.getPremisesById(premisesIdsToProcess); + Node destinationNode = nodeRepository.getById(dto.getDestinationNodeId()).orElseThrow(); + + + var destinations = new ArrayList(); + + for(var premise : premisesToProcess) { + var destination = new Destination(); + destination.setDestinationNodeId(dto.getDestinationNodeId()); + destination.setPremiseId(premise.getId()); + destination.setAnnualAmount(BigDecimal.ZERO); + destination.setD2d(false); + destination.setDisposalCost(null); + destination.setHandlingCost(null); + destination.setRepackingCost(null); + destination.setRateD2d(BigDecimal.ZERO); + destination.setId(destinationRepository.insert(destination)); + + Node source = premise.getSupplierNodeId() == null ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow(); + findRouteAndSave(destination.getId(), destinationNode, source, premise.getSupplierNodeId() == null); + destinations.add(destination); + } + + return destinations.stream().collect(Collectors.toMap(Destination::getPremiseId, destinationTransformer::toDestinationDTO)); - return null; } public DestinationDTO getDestination(Integer id) { @@ -63,6 +100,44 @@ public class DestinationService { destinationRepository.update(id, userId, destinationUpdateDTO.getRepackingCost(), destinationUpdateDTO.getDisposalCost(), destinationUpdateDTO.getHandlingCost()); } + + @Transactional + public void findRouteAndSave(Integer destinationId, Node destination, Node supplier, boolean isUserSupplierNode) { + + var routeObjs = routingService.findRoutes(destination, supplier, isUserSupplierNode); + + for (var routeObj : routeObjs) { + boolean first = true; + Integer fromNodeId = null; + + var premiseRoute = routeObj.getRoute(); + premiseRoute.setDestinationId(destinationId); + int routeId = routeRepository.insert(premiseRoute); + + + for (RouteSectionInformation section : routeObj.getSections()) { + + if (first) { + fromNodeId = routeNodeRepository.insert(section.getFromNode()); + first = false; + } + + var toNode = section.getToNode(); + Integer toNodeId = routeNodeRepository.insert(toNode); + + var premiseRouteSection = section.getSection(); + + premiseRouteSection.setRouteId(routeId); + premiseRouteSection.setFromRouteNodeId(fromNodeId); + premiseRouteSection.setToRouteNodeId(toNodeId); + + routeSectionRepository.insert(premiseRouteSection); + + fromNodeId = toNodeId; + } + } + } + @Transactional public void deleteAllDestinationsByPremiseId(List ids, boolean deleteRoutesOnly) { ids.forEach(id -> deleteDestinationById(id, deleteRoutesOnly)); @@ -96,5 +171,35 @@ public class DestinationService { } + @Transactional + public void duplicate(Integer fromPremiseId, Integer toPremiseId) { + List destinations = destinationRepository.getByPremiseId(fromPremiseId); + + for(Destination destination : destinations) { + + destination.setPremiseId(toPremiseId); + Integer destinationId = destinationRepository.insert(destination); + + for(Route route : routeRepository.getByDestinationId(destination.getId())) { + route.setDestinationId(destinationId); + Integer routeId = routeRepository.insert(route); + + for(RouteSection section : routeSectionRepository.getByRouteId(route.getId())) { + section.setRouteId(routeId); + + RouteNode fromNode = routeNodeRepository.getById(section.getFromRouteNodeId()).orElseThrow(); + RouteNode toNode = routeNodeRepository.getById(section.getToRouteNodeId()).orElseThrow(); + + Integer fromNodeId = routeNodeRepository.insert(fromNode); + Integer toNodeId = routeNodeRepository.insert(toNode); + + section.setFromRouteNodeId(fromNodeId); + section.setToRouteNodeId(toNodeId); + + routeSectionRepository.insert(section); + } + } + } + } } diff --git a/src/main/java/de/avatic/lcc/service/access/PremisesService.java b/src/main/java/de/avatic/lcc/service/access/PremisesService.java index 9a8bb7d..a99f76f 100644 --- a/src/main/java/de/avatic/lcc/service/access/PremisesService.java +++ b/src/main/java/de/avatic/lcc/service/access/PremisesService.java @@ -7,7 +7,7 @@ import de.avatic.lcc.dto.calculation.edit.masterData.PackagingUpdateDTO; import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO; import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import de.avatic.lcc.repositories.pagination.SearchQueryResult; -import de.avatic.lcc.repositories.premise.*; +import de.avatic.lcc.repositories.premise.PremiseRepository; import de.avatic.lcc.service.transformer.generic.DimensionTransformer; import de.avatic.lcc.service.transformer.premise.PremiseTransformer; import org.springframework.stereotype.Service; @@ -16,7 +16,6 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; -import java.util.stream.Stream; @Service public class PremisesService { @@ -26,7 +25,7 @@ public class PremisesService { private final DimensionTransformer dimensionTransformer; private final DestinationService destinationService; - public PremisesService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DimensionTransformer dimensionTransformer, DestinationService destinationService, DestinationRepository destinationRepository, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, DestinationService destinationService) { + public PremisesService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DimensionTransformer dimensionTransformer, DestinationService destinationService) { this.premiseRepository = premiseRepository; this.premiseTransformer = premiseTransformer; this.dimensionTransformer = dimensionTransformer; @@ -53,7 +52,7 @@ public class PremisesService { //TODO move to other service public Integer startCalculation(List premises) { - + return null; } public HashMap updatePackaging(PackagingUpdateDTO packagingDTO) { diff --git a/src/main/java/de/avatic/lcc/service/calculation/ChangeMaterialService.java b/src/main/java/de/avatic/lcc/service/calculation/ChangeMaterialService.java index f601841..cc15c0a 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/ChangeMaterialService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/ChangeMaterialService.java @@ -2,26 +2,103 @@ package de.avatic.lcc.service.calculation; import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; import de.avatic.lcc.dto.calculation.edit.SetDataDTO; +import de.avatic.lcc.model.packaging.PackagingDimension; +import de.avatic.lcc.model.premises.Premise; +import de.avatic.lcc.model.properties.PackagingProperty; +import de.avatic.lcc.model.properties.PackagingPropertyMappingId; +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.users.UserNodeRepository; +import de.avatic.lcc.service.CustomApiService; +import de.avatic.lcc.service.access.PremisesService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; +import java.util.*; @Service public class ChangeMaterialService { + private final PremiseRepository premiseRepository; + private final PremisesService premisesService; + private final CustomApiService customApiService; + private final NodeRepository nodeRepository; + private final UserNodeRepository userNodeRepository; + private final PackagingRepository packagingRepository; + private final PackagingDimensionRepository packagingDimensionRepository; + private final PackagingPropertiesRepository packagingPropertiesRepository; + + public ChangeMaterialService(PremiseRepository premiseRepository, PremisesService premisesService, CustomApiService customApiService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository) { + this.premiseRepository = premiseRepository; + this.premisesService = premisesService; + this.customApiService = customApiService; + this.nodeRepository = nodeRepository; + this.userNodeRepository = userNodeRepository; + this.packagingRepository = packagingRepository; + this.packagingDimensionRepository = packagingDimensionRepository; + this.packagingPropertiesRepository = packagingPropertiesRepository; + } + @Transactional - public List setMaterial(SetDataDTO setMaterialDTO) { - //0. check if one of the given premises contains the same supplier id, and abort if any duplicate would emerge + public List setMaterial(SetDataDTO dto) { + Integer userId = 1; /* TODO get user id */ - //3 if updateMasterDate is set: - //3.1 copy packaging data (if exists) of new material to premise - //3.2 set new tariff rate and hs code (if supplier country changed) + Integer materialId = dto.getMaterialId(); + List premiseIds = dto.getPremiseId(); - //4. Deliver Premise Detail + if (materialId == null || premiseIds == null || premiseIds.isEmpty()) + throw new IllegalArgumentException("No supplier supplierNodeId or premises given"); + + // get all premises first. + List allPremises = premiseRepository.getPremisesById(premiseIds); + + // find resulting duplicates, split into "keep" and "to be deleted". + Map uniqueMap = new HashMap<>(); + List premisesToBeDeleted = new ArrayList<>(); + allPremises.forEach(p -> { + if (null != uniqueMap.putIfAbsent(p.getMaterialId(), p)) premisesToBeDeleted.add(p); + }); + Collection premisesToProcess = uniqueMap.values(); + + // check if user owns all premises: + if (allPremises.stream().anyMatch(p -> !p.getUserId().equals(userId))) + throw new IllegalArgumentException("Not authorized to change suppliers of premises owned by other users"); + + // check for any other collisions, and mark as "to be deleted": + premisesToBeDeleted.addAll(premisesToProcess.stream().map(p -> premiseRepository.getCollidingPremisesOnChange(userId, p.getId(), materialId, p.getSupplierNodeId())).flatMap(List::stream).toList()); + + if(dto.isUpdateMasterData()) { + for (var premise : premisesToProcess) { + var countryId = dto.isUserSupplierNode() ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow().getCountryId() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow().getCountryId(); + var tariffRate = customApiService.getTariffRate(premise.getHsCode(), countryId); + premiseRepository.updateTariffRate(premise.getId(), tariffRate); + + if (!dto.isUserSupplierNode()) { + var packaging = packagingRepository.getByMaterialIdAndSupplierId(premise.getMaterialId(), dto.getSupplierNodeId()); + Optional dimension = packagingDimensionRepository.getById(packaging.getFirst().getHuId()); + + if (dimension.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(premise.getId()), userId, dimension.get(), stackable, mixable); + } + } + + } + } + + // actually update materialId. + premiseRepository.setMaterialId(premisesToProcess.stream().map(Premise::getId).toList(), materialId); - return null; + //delete all conflicting premises: + premisesService.delete(premisesToBeDeleted.stream().map(Premise::getId).toList()); + + + return premisesService.getPremises(premisesToProcess.stream().map(Premise::getId).toList()); } } diff --git a/src/main/java/de/avatic/lcc/service/calculation/ChangeSupplierService.java b/src/main/java/de/avatic/lcc/service/calculation/ChangeSupplierService.java index 08b76d5..4468bdf 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/ChangeSupplierService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/ChangeSupplierService.java @@ -6,15 +6,14 @@ 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.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.DestinationRepository; -import de.avatic.lcc.repositories.premise.PremiseRepository; +import de.avatic.lcc.repositories.premise.*; import de.avatic.lcc.repositories.users.UserNodeRepository; import de.avatic.lcc.service.CustomApiService; import de.avatic.lcc.service.access.DestinationService; @@ -34,13 +33,15 @@ public class ChangeSupplierService { private final CustomApiService customApiService; private final NodeRepository nodeRepository; private final UserNodeRepository userNodeRepository; - private final MaterialRepository materialRepository; private final PackagingRepository packagingRepository; private final PackagingDimensionRepository packagingDimensionRepository; private final PackagingPropertiesRepository packagingPropertiesRepository; private final DestinationRepository destinationRepository; + private final RouteRepository routeRepository; + private final RouteSectionRepository routeSectionRepository; + private final RouteNodeRepository routeNodeRepository; - public ChangeSupplierService(PremiseRepository premiseRepository, DestinationService destinationService, RoutingService routingService, PremisesService premisesService, CustomApiService customApiService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository, MaterialRepository materialRepository, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, DestinationRepository destinationRepository) { + public ChangeSupplierService(PremiseRepository premiseRepository, DestinationService destinationService, RoutingService routingService, PremisesService premisesService, CustomApiService customApiService, NodeRepository nodeRepository, UserNodeRepository userNodeRepository, PackagingRepository packagingRepository, PackagingDimensionRepository packagingDimensionRepository, PackagingPropertiesRepository packagingPropertiesRepository, DestinationRepository destinationRepository, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository) { this.premiseRepository = premiseRepository; this.destinationService = destinationService; this.routingService = routingService; @@ -48,11 +49,13 @@ public class ChangeSupplierService { this.customApiService = customApiService; this.nodeRepository = nodeRepository; this.userNodeRepository = userNodeRepository; - this.materialRepository = materialRepository; this.packagingRepository = packagingRepository; this.packagingDimensionRepository = packagingDimensionRepository; this.packagingPropertiesRepository = packagingPropertiesRepository; this.destinationRepository = destinationRepository; + this.routeRepository = routeRepository; + this.routeSectionRepository = routeSectionRepository; + this.routeNodeRepository = routeNodeRepository; } @@ -92,8 +95,9 @@ public class ChangeSupplierService { //recalculate routes: for (Premise premise : premisesToProcess) { List destination = destinationRepository.getByPremiseId(premise.getId()); - for( Destination d : destination ) { - routingService.findRoutes(d.getId(), supplierNodeId); + for (Destination d : destination) { + Node destinationNode = nodeRepository.getById(d.getDestinationNodeId()).orElseThrow(); + destinationService.findRouteAndSave(d.getId(), destinationNode, supplier, dto.isUserSupplierNode() ); } } @@ -108,15 +112,11 @@ public class ChangeSupplierService { Optional dimension = packagingDimensionRepository.getById(packaging.getFirst().getHuId()); if (dimension.isPresent()) { - Optional stackable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.getFirst().getId(), PackagingPropertyMappingId.STACKABLE.name()); - Optional mixable = packagingPropertiesRepository.getByPackagingIdAndType(packaging.getFirst().getId(), PackagingPropertyMappingId.MIXABLE.name()); - //TODO check optional - premiseRepository.updatePackaging(Collections.singletonList(premise.getId()), userId, dimension.get(), Boolean.valueOf(stackable.get().getValue()), Boolean.valueOf(mixable.get().getValue())); + 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(premise.getId()), userId, dimension.get(), stackable, mixable); } - } - - premisesService.fillPackaging(premisesToProcess.stream().map(Premise::getId).toList()); } } @@ -132,4 +132,6 @@ public class ChangeSupplierService { } + + } diff --git a/src/main/java/de/avatic/lcc/service/calculation/PremiseCreationService.java b/src/main/java/de/avatic/lcc/service/calculation/PremiseCreationService.java index a56579e..11add89 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/PremiseCreationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/PremiseCreationService.java @@ -1,13 +1,139 @@ package de.avatic.lcc.service.calculation; import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; +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.repositories.premise.PremiseRepository; +import de.avatic.lcc.repositories.premise.RouteRepository; +import de.avatic.lcc.service.access.DestinationService; +import de.avatic.lcc.service.transformer.premise.PremiseTransformer; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; @Service public class PremiseCreationService { + private final PremiseRepository premiseRepository; + private final PremiseTransformer premiseTransformer; + private final DestinationService destinationService; + + public PremiseCreationService(PremiseRepository premiseRepository, PremiseTransformer premiseTransformer, DestinationService destinationService) { + this.premiseRepository = premiseRepository; + this.premiseTransformer = premiseTransformer; + this.destinationService = destinationService; + } + public List createPremises(List materialIds, List supplierIds, List userSupplierIds, boolean createEmpty) { - return null; + + Integer userId = 1; //TODO get user id + + List foundPremises = new ArrayList<>(premiseRepository.getPremisesByMaterialIdsAndSupplierIds(materialIds, supplierIds, userSupplierIds, userId, createEmpty).stream().map(TemporaryPremise::new).toList()); + + List toBeCopied = foundPremises.stream().filter(p -> (!p.getPremise().getState().equals(PremiseState.DRAFT) || !Objects.equals(p.getPremise().getUserId(), userId))).toList(); + + List toBeCreated = materialIds.stream() + .flatMap(materialId -> { + Stream supplierCombinations = supplierIds.stream() + .map(supplierId -> new TemporaryPremise(materialId, supplierId, null, false)); + + Stream userSupplierCombinations = + (userSupplierIds != null && !userSupplierIds.isEmpty()) ? + userSupplierIds.stream() + .map(userSupplierId -> new TemporaryPremise(materialId, null, userSupplierId, true)) : + Stream.empty(); + + 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(); + + List 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); + } + + return premiseRepository.getPremisesById(premiseIds).stream().map(premiseTransformer::toPremiseDetailDTO).toList(); + } + + + + private static class TemporaryPremise { + private Integer materialId; + private Integer supplierId; + private Integer userSupplierId; + private boolean isUserSupplier; + + private Premise premise; + + public Integer getMaterialId() { + return materialId; + } + + public void setMaterialId(Integer materialId) { + this.materialId = materialId; + } + + public Integer getSupplierId() { + return supplierId; + } + + public void setSupplierId(Integer supplierId) { + this.supplierId = supplierId; + } + + public Integer getUserSupplierId() { + return userSupplierId; + } + + public void setUserSupplierId(Integer userSupplierId) { + this.userSupplierId = userSupplierId; + } + + public boolean isUserSupplier() { + return isUserSupplier; + } + + public void setUserSupplier(boolean userSupplier) { + isUserSupplier = userSupplier; + } + + 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 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 Premise getPremise() { + return premise; + } } } diff --git a/src/main/java/de/avatic/lcc/service/calculation/PremiseSearchStringAnalyzerService.java b/src/main/java/de/avatic/lcc/service/calculation/PremiseSearchStringAnalyzerService.java index 6f280f6..ad80421 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/PremiseSearchStringAnalyzerService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/PremiseSearchStringAnalyzerService.java @@ -1,12 +1,101 @@ package de.avatic.lcc.service.calculation; import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO; +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.service.transformer.generic.MaterialTransformer; +import de.avatic.lcc.service.transformer.generic.NodeTransformer; import org.springframework.stereotype.Service; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Service for analyzing search strings and retrieving materials and their associated suppliers. + * This service uses repositories and transformers to process and map database entities + * to DTOs for the result. + */ @Service public class PremiseSearchStringAnalyzerService { + private static final String PART_NUMBER_REGEX = "([a-zA-Z0-9][a-zA-Z0-9\\-]{4,11})"; + private static final Pattern PART_NUMBER_PATTERN = Pattern.compile(PART_NUMBER_REGEX); + private final MaterialRepository materialRepository; + private final NodeRepository nodeRepository; + private final PremiseRepository premiseRepository; + private final NodeTransformer nodeTransformer; + private final MaterialTransformer materialTransformer; + + /** + * Constructor for the PremiseSearchStringAnalyzerService, initializing the required repositories + * and transformers for analyzing search strings and processing materials and suppliers. + * + * @param materialRepository Repository for accessing material entities from the database. + * @param nodeRepository Repository for accessing node entities (suppliers) from the database. + * @param premiseRepository Repository for accessing premise-related data, including suppliers associated with materials. + * @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) { + this.materialRepository = materialRepository; + this.nodeRepository = nodeRepository; + this.premiseRepository = premiseRepository; + this.nodeTransformer = nodeTransformer; + this.materialTransformer = materialTransformer; + } + + /** + * Analyzes the provided search string to find materials and associated suppliers. + * Processes the search string to extract part numbers, retrieves the associated materials, + * and identifies the suppliers connected to the materials. These results are returned + * in the form of a DTO containing lists of materials and suppliers. + * + * @param search The search string potentially containing part numbers used to find + * materials and their associated suppliers. + * + * @return A {@code PremiseSearchResultDTO} containing lists of material DTOs, suppliers, + * and user-specific suppliers related to the identified materials. + */ public PremiseSearchResultDTO findMaterialAndSuppliers(String search) { - return null; + List material = materialRepository.getByPartNumbers(findPartNumbers(search)); + List materialIds = material.stream().map(Material::getId).toList(); + + // find suppliers associated with this material. + List supplierIds = premiseRepository.findAssociatedSuppliers(materialIds); + List userSupplierIds = premiseRepository.findAssociatedUserSuppliers(materialIds); + + 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()); + + return dto; + } + + /** + * Extracts part numbers from the provided search string based on a predefined pattern. + * + * @param search The input string that potentially contains part numbers to be extracted. + * @return A collection of unique part numbers found within the input string. + */ + public static Collection findPartNumbers(String search) { + Set partNumbers = new HashSet<>(); + Matcher matcher = PART_NUMBER_PATTERN.matcher(search); + + // Find all matches + while (matcher.find()) { + // Get the match from group 1 (inside the lookahead) + String partNumber = matcher.group(1); + partNumbers.add(partNumber); + } + + return partNumbers; } } diff --git a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java index 760fe32..796d2a1 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java @@ -1,10 +1,8 @@ package de.avatic.lcc.service.calculation; +import de.avatic.lcc.dto.generic.RouteType; import de.avatic.lcc.model.nodes.Node; -import de.avatic.lcc.model.premises.route.Route; -import de.avatic.lcc.model.premises.route.RouteInformation; -import de.avatic.lcc.model.premises.route.RouteNode; -import de.avatic.lcc.model.premises.route.RouteSection; +import de.avatic.lcc.model.premises.route.*; import de.avatic.lcc.model.properties.SystemPropertyMappingId; import de.avatic.lcc.model.rates.ContainerRate; import de.avatic.lcc.model.rates.ContainerRateType; @@ -13,49 +11,43 @@ import de.avatic.lcc.repositories.NodeRepository; import de.avatic.lcc.repositories.properties.PropertyRepository; import de.avatic.lcc.repositories.rates.ContainerRateRepository; import de.avatic.lcc.repositories.rates.MatrixRateRepository; -import de.avatic.lcc.repositories.users.UserNodeRepository; import org.springframework.stereotype.Service; -import java.math.BigDecimal; import java.util.*; +import java.util.stream.Collectors; @Service public class RoutingService { - private final NodeRepository nodeRepository; private final MatrixRateRepository matrixRateRepository; - private final PropertyRepository propertyRepository; private final ChainResolver chainResolver; + private final NodeRepository nodeRepository; private final ContainerRateRepository containerRateRepository; private final DistanceService distanceService; - private final UserNodeRepository userNodeRepository; + private final PropertyRepository propertyRepository; - public RoutingService(NodeRepository nodeRepository, MatrixRateRepository matrixRateRepository, PropertyRepository propertyRepository, ChainResolver chainResolver, ContainerRateRepository containerRateRepository, DistanceService distanceService, UserNodeRepository userNodeRepository) { - this.nodeRepository = nodeRepository; + public RoutingService(MatrixRateRepository matrixRateRepository, ChainResolver chainResolver, NodeRepository nodeRepository, ContainerRateRepository containerRateRepository, DistanceService distanceService, PropertyRepository propertyRepository) { this.matrixRateRepository = matrixRateRepository; - this.propertyRepository = propertyRepository; this.chainResolver = chainResolver; + this.nodeRepository = nodeRepository; this.containerRateRepository = containerRateRepository; this.distanceService = distanceService; - this.userNodeRepository = userNodeRepository; + this.propertyRepository = propertyRepository; } - public List findRoutes(Integer destinationId, Integer sourceId, boolean isUserNode) { - List rios = new ArrayList<>(); + public List findRoutes(Node destination, Node source, boolean sourceIsUserNode) { + List routeInformation = new ArrayList<>(); + TemporaryContainer container = new TemporaryContainer(source, destination); /* - * 1. STEP: * Get the source and destination node from database. * Check if there is a matrix rate for the source country. */ - Node source = (isUserNode) ? userNodeRepository.getById(sourceId).orElseThrow() : nodeRepository.getById(sourceId).orElseThrow(); - Node destination = nodeRepository.getById(destinationId).orElseThrow(); + matrixRateRepository.getByCountryIds(source.getCountryId(), source.getCountryId()).ifPresent(container::setSourceMatrixRate); - Optional sourceMatrixRate = matrixRateRepository.getByCountryIds(source.getCountryId(), source.getCountryId()); /* - * 2. STEP: * Generate recursive all chains starting with the destination node. * This means all chains within the last node of a chain are connected to the * existing chain. @@ -65,213 +57,341 @@ public class RoutingService { * * Then get all countries from the end of the destination chains. */ - List> destinationChains = chainResolver.buildChains(destinationId); + + List> destinationChains = chainResolver.buildChains(destination.getId()); List inboundCountries = destinationChains.stream().filter(chain -> !chain.isEmpty()).map(chain -> chain.getLast().getCountryId()).distinct().toList(); /* - * 3. STEP: * Get all outbound nodes for the country of the source node. In this first step this includes: * - all intermediate nodes that have the same country id. * - all nodes that are explicitly mapped as outbound node for this country id. - * - * Then find all outbound nodes that have main transports to the inbound countries found in the step before - * - * Create a route information object for each outbound node that have a main transport, also add all post-runs - * that can be used for the main run. - * - * Lastly mapping the destination chain on the route information objects, based on there compatibility: - * - * - last and second-to-last node in the destination chain are identical with the end of the main run and the - * end of the post-run. -> High Quality. - * - last in the destination chain are identical with the - * end of the post-run. -> Medium Quality - * - last in the destination chain in the same country as the - * end of the post-run. -> Low Quality - * */ List outboundNodes = nodeRepository.getAllOutboundFor(source.getCountryId()); - List mainRuns = containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(outboundNodes.stream().map(Node::getId).toList(), inboundCountries); - - mainRuns.forEach(mainRun -> rios.add(new RouteInformationObject(destination, source, mainRun, nodeRepository.getById(mainRun.getToNodeId()).orElseThrow(), outboundNodes.stream().filter(n -> n.getId().equals(mainRun.getFromNodeId())).findFirst().orElseThrow(), containerRateRepository.getPostRunsFor(mainRun)))); - addDestinationChains(rios, destinationChains); /* - * 4. STEP: - * Next we are resolving the predecessor chains of all outbound nodes recursively in exact the - * same way the destination chains were resolved. + * Find main runs based on the outbound nodes and the inbound countries found before. + * Find post-runs for the main runs. + * + * Store all information in the TemporaryContainer object. */ - getSourceCountryChains(rios); + container.setMainRuns(outboundNodes.stream().collect(Collectors.toMap(Node::getId, n -> containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(n.getId(), inboundCountries)))); + container.setPostRuns(container.getMainRuns().stream().collect(Collectors.toMap(ContainerRate::getId, containerRateRepository::getPostRunsFor))); + + + connectDestinationChainAndMainRun(container, outboundNodes, destinationChains); + connectSourceChainAndSource(container); /* - * 5. STEP: - * Try to connect the destination chains directly with the source node with a - * matrix rate. All routes that are constructed in this way are getting its own - * route information object and added to the list. + * At this point all routes with a main run are created. + * We find now the best route per main run and throw away the rest. */ - rios.addAll(findRoutesWithDestinationChainsAndSource(destinationChains, source, destination)); + findCheapestPerMainRun(container); /* - * 6. STEP: - * Final step is to check if there are actual rates for the pre-run and post-run - * chains, and filter out all without rates. - * - * pre-run chains also needs to be checked if they are connectable to the supplier, - * all unconnectable chains are thrown away. - * - * - * + * Now we also create routes without main run (matrix rate) */ + connectDestinationChainDirectly(container, destinationChains); - Set containerRates = new HashSet<>(); - Set matrixRates = new HashSet<>(); + /* + * finally find and mark the fastest and the cheapest route. + */ + findAndMarkCheapestAndFastest(container); - List nearByNodes = nodeRepository.getByDistance(source, getRegionRadius()); + /* + * Convert to Database model + */ + for (var route : container.getRoutes()) { + RouteInformation routeInformationObj = new RouteInformation(); - List routeInformationList = new ArrayList<>(); - - for (var rio : rios) { - - - processDestinationChains(rio, containerRates, matrixRates); - - - processSourceCountryChains(rio, containerRates, matrixRates, sourceMatrixRate.orElse(null), nearByNodes, source); - - rio.setSource(source, isUserNode); - - routeInformationList.add(rio.getRouteInformation(containerRates, matrixRates)); + routeInformationObj.setRoute(mapRoute(route)); + routeInformationObj.setSections(mapSections(route.getSections(), sourceIsUserNode)); + routeInformation.add(routeInformationObj); } - return routeInformationList; - + return routeInformation; } - private boolean nodesConnectable(Node startNode, Node endNode, ChainInformationObject chain, Set containerRates, Set matrixRates) { - var containerSection = new ChainSectionContainerRateInformation(startNode, endNode); - var matrixSection = new ChainSectionMatrixRateInformation(startNode.getCountryId(), endNode.getCountryId()); + private RouteNode mapNode(Node node, boolean isUserNode) { + RouteNode routeNode = new RouteNode(); - if (!containerRates.contains(containerSection) && !matrixRates.contains(matrixSection)) { - Optional rate = containerRateRepository.findRoute(startNode.getId(), endNode.getId(), ContainerRateType.ROAD); + routeNode.setName(node.getName()); + routeNode.setCountryId(node.getCountryId()); + routeNode.setAddress(node.getAddress()); + routeNode.setGeoLng(node.getGeoLng()); + routeNode.setGeoLat(node.getGeoLat()); + routeNode.setUserNodeId(isUserNode? node.getId() : null); + routeNode.setNodeId(isUserNode ? null : node.getId()); + routeNode.setIntermediate(node.getIntermediate() != null ? node.getIntermediate() : false); + routeNode.setDestination(node.getDestination() != null ? node.getIntermediate() : false); + routeNode.setSource(node.getSource() != null ? node.getIntermediate() : false); + routeNode.setOutdated(node.getDeprecated()); - if (rate.isPresent()) { - containerSection.setRate(rate.get()); - containerRates.add(containerSection); - chain.addSectionInformation(new ChainSectionInformation(startNode, endNode, containerSection)); - } else if (!matrixRates.contains(matrixSection)) { - Optional matrixRate = matrixRateRepository.getByCountryIds(startNode.getCountryId(), endNode.getCountryId()); + return routeNode; + } - if (matrixRate.isPresent()) { - matrixSection.setRate(matrixRate.get()); - matrixSection.setDistance(distanceService.getDistance(startNode, endNode, true)); - matrixRates.add(matrixSection); - chain.addSectionInformation(new ChainSectionInformation(startNode, endNode, containerSection)); - } else { - return false; - } + private Route mapRoute(TemporaryRouteObject route) { + Route routeObj = new Route(); + + routeObj.setCheapest(route.isCheapest()); + routeObj.setFastest(route.isFastest()); + + return routeObj; + } + + private List mapSections(List sections, boolean sourceIsUserNode) { + int index = 1; + + List routeSections = new ArrayList<>(); + boolean passedMainRun = false; + boolean firstSection = true; + + RouteNode fromNode = null; + for(var section : sections.reversed()) { + var routeSection = mapSection(section); + + if(firstSection) { + fromNode = mapNode(section.getFromNode(),sourceIsUserNode); + firstSection = false; + } + + var toNode = mapNode(section.getToNode(), false); + + if (!section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN) && !section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN)) { + if (!passedMainRun) + routeSection.setPreRun(true); + + if (passedMainRun) + routeSection.setPostRun(true); + + } + + if(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN) || section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN)) + passedMainRun = true; + + routeSection.setListPosition(index++); + routeSections.add(new RouteSectionInformation(routeSection, fromNode, toNode)); + fromNode = toNode; + } + + return routeSections; + } + + private RouteSection mapSection(TemporaryRateObject section) { + RouteSection routeSection = new RouteSection(); + + routeSection.setTransportType(mapRouteType(section)); + routeSection.setFromRouteNodeId(section.getFromNode().getId()); + routeSection.setToRouteNodeId(section.getToNode().getId()); + routeSection.setMainRun(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN)); + routeSection.setOutdated(false); + + routeSection.setPostRun(false); + routeSection.setPreRun(false); + + return routeSection; + + } + + private RouteType mapRouteType(TemporaryRateObject rate) { + + switch(rate.getType()) { + case MATRIX, CONTAINER -> { + return RouteType.ROAD; + } + case POST_RUN -> { + return RouteType.POST_RUN; + } + case MAIN_RUN -> { + return RouteType.valueOf(rate.getContainerRateTye().name()); } } - return true; + return null; } - private void processSourceCountryChains(RouteInformationObject rio, Set containerRates, Set matrixRates, MatrixRate sourceMatrixRate, List nearByNodes, Node source) { + private void findAndMarkCheapestAndFastest(TemporaryContainer container) { + var routes = container.getRoutes(); - for (var chain : rio.getSourceCountryChains()) { - boolean directly = false; - Node nearByNode = null; - boolean useSourceMatrixRate = false; + TemporaryRouteObject cheapest = null; + double cheapestCost = Double.MAX_VALUE; - // 1. try connect chain end to source node directly - if (!nodesConnectable(source, chain.getChain().getLast(), chain, containerRates, matrixRates)) { + TemporaryRouteObject fastest = null; + double fastestRoute = Double.MAX_VALUE; - // 2. try to connect via matrix rate - useSourceMatrixRate = sourceMatrixRate != null && chain.getChain().getLast().getCountryId().equals(sourceMatrixRate.getFromCountry()); + for(var route : routes) { + double routeCost = route.getCost(); + int leadTime = route.getLeadTime(); - if (!useSourceMatrixRate) { - // try to connect via near by nodes (metropolitan). - nearByNode = nearByNodes.stream().filter(n -> nodesConnectable(n, chain.getChain().getLast(), chain, containerRates, matrixRates)).findFirst().orElse(null); - if (null != nearByNode) { - chain.addNearByNode(nearByNode); - } - } - } else directly = true; + if(routeCost < cheapestCost) { + cheapestCost = routeCost; + cheapest = route; + } - if (nearByNode != null || useSourceMatrixRate || directly) { - for (int idx = chain.getChain().size() - 1; idx > 0; idx--) { - Node startNode = chain.getChain().get(idx); - Node endNode = chain.getChain().get(idx - 1); + if(leadTime < fastestRoute) { + fastestRoute = leadTime; + fastest = route; + } + } - if (!nodesConnectable(startNode, endNode, chain, containerRates, matrixRates)) - rio.removeSourceChain(chain); - } - } else rio.removeSourceChain(chain); + if(cheapest != null) { + cheapest.setCheapest(); + } + + if(fastest != null) { + fastest.setFastest(); } } - private void processDestinationChains(RouteInformationObject rio, Set containerRates, Set matrixRates) { - List chains = rio.getDestinationChains(); + private void findCheapestPerMainRun(TemporaryContainer container) { + + var routesByMainRun = container.getRoutes().stream().collect(Collectors.groupingBy(TemporaryRouteObject::getMainRun)); + Collection cheapestRoutes = new ArrayList<>(); + + for(var mainRun : routesByMainRun.keySet()) { + List routes = routesByMainRun.get(mainRun); + TemporaryRouteObject cheapest = null; + double cheapestCost = Double.MAX_VALUE; + + for(var route : routes) { + double routeCost = route.getCost(); + + if(routeCost < cheapestCost) { + cheapestCost = routeCost; + cheapest = route; + } + } + + if(cheapest != null) { + cheapestRoutes.add(cheapest); + } + } + + container.overrideRoutes(cheapestRoutes); + } + + private void connectDestinationChainDirectly(TemporaryContainer container, List> chains) { + Collection rates = container.getRates(); for (var chain : chains) { + var toNode = chain.isEmpty() ? container.getDestinationNode() : chain.getLast(); - for (int idx = chain.getChain().size() - 1; idx > chain.getQuality().getStartIdx(); idx--) { - Node startNode = chain.getChain().get(idx); - Node endNode = chain.getChain().get(idx - 1); + TemporaryRateObject finalSection = new TemporaryRateObject(container.getSourceNode(), toNode, TemporaryRateObject.TemporaryRateObjectType.MATRIX); - if (!nodesConnectable(startNode, endNode, chain, containerRates, matrixRates)) - rio.removeDestinationChain(chain); + if (rates.contains(finalSection)) { + TemporaryRateObject finalMatrixRateObj = finalSection; + finalSection = container.getRates().stream().filter(r -> r.equals(finalMatrixRateObj)).findFirst().orElse(null); + } else { + var matrixRate = matrixRateRepository.getByCountryIds(container.getSourceNode().getCountryId(), toNode.getCountryId()).orElse(null); + // no matrix rate in database. skip this chain + if (matrixRate == null) { + continue; + } + + finalSection.setRate(matrixRate); + finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true)); + rates.add(finalSection); } + + // could not create an temporary rate object -> so just skip here ... (should not happen) + if (finalSection == null) { + continue; + } + + // create a route. + boolean routable = true; + TemporaryRouteObject routeObj = new TemporaryRouteObject(); + + for (int idx = 1; idx < chain.size(); idx++) { + Node startNode = chain.get(idx); + Node endNode = chain.get(idx - 1); + + var rate = connectNodes(startNode, endNode, container); + + if (rate != null) { + routeObj.addSection(rate); + } else { + // chain is not routable -> discard + routable = false; + break; + } + } + + // if the chain is routable -> add the final rate and save the route. + if (routable) { + routeObj.addSection(finalSection); + container.addRoute(routeObj); + } + } } - /** - * Adds destination chains to the given list of RouteInformationObject instances based - * on their connection properties and quality. The method iterates through each RouteInformationObject - * and assesses its post-run connections with possible destination chains, adding the chains with an - * associated connection quality determined by their compatibility. - * - * @param rios The list of RouteInformationObject instances to which the destination chains - * will be added. Each RouteInformationObject contains post-run connection details. - * @param destinationChains A list of destination chains, where each chain is represented - * as a list of Node objects. These chains are evaluated for - * addition to the RouteInformationObjects based on connection compatibility. - */ - private void addDestinationChains(List rios, List> destinationChains) { + private TemporaryRateObject connectNearByNodes(Node chainEnd, List nearByNodes, TemporaryContainer container) { - for (var rio : rios) { - for (var chain : destinationChains) { - for (var postRun : rio.getPostRuns()) { - if (chain.getLast().getId().equals(postRun.getToNodeId())) { - rio.addDestinationChain(new ChainInformationObject(chain, ChainConnectionQuality.MEDIUM)); - } else if (chain.getLast().getId().equals(postRun.getFromNodeId()) && chain.get(chain.size() - 2).getId().equals(postRun.getToNodeId())) { - rio.addDestinationChain(new ChainInformationObject(chain, ChainConnectionQuality.HIGH)); - } else if (chain.getLast().getCountryId().equals(postRun.getToCountryId())) { - rio.addDestinationChain(new ChainInformationObject(chain, ChainConnectionQuality.LOW)); + + for (var nearbyNode : nearByNodes) { + TemporaryRateObject nearByRate = connectNodes(nearbyNode, chainEnd, container); + if (null != nearByRate) { + return nearByRate; + } + } + return null; + } + + private void connectSourceChainAndSource(TemporaryContainer container) { + + /* get the near-by nodes if no country matrix rate present */ + List nearByNodes = (container.hasSourceMatrixRate()) ? null : nodeRepository.getByDistance(container.getSourceNode(), getRegionRadius()); + + Collection routes = new ArrayList<>(); + + for (var route : container.getRoutes()) { + var mainRun = route.getMainRun(); + var sourceChains = chainResolver.buildChains(mainRun.getFromNode().getId()); + + + for (var chain : sourceChains) { + Node source = container.getSourceNode(); + boolean chainEndIsSource = source.getId().equals(chain.getLast().getId()); + + // find final section: check if chain end and source node are identical, then check if chain end can be connected to + // source node, if this is not possible use a near-by node + TemporaryRateObject finalSection = (chainEndIsSource) ? null : connectNodes(source, chain.getLast(), container); + finalSection = ((finalSection == null && !chainEndIsSource && nearByNodes != null) ? connectNearByNodes(chain.getLast(), nearByNodes, container) : finalSection); + + if (finalSection != null || chainEndIsSource) { + boolean routable = true; + TemporaryRouteObject duplicate = route.clone(); + + for (int idx = 1; idx < chain.size() - 1; idx++) { + Node startNode = chain.get(idx); + Node endNode = chain.get(idx - 1); + + TemporaryRateObject rate = connectNodes(startNode, endNode, container); + + if (rate != null) duplicate.addSection(rate); + else { + routable = false; + break; + } + } + + if (routable) { + if (finalSection != null) { + // add final section if necessary, + // if last chain node == source node this can be skipped. + duplicate.addSection(finalSection); + if (!finalSection.getFromNode().getId().equals(source.getId())) duplicate.routeOverNearBy(); + } + routes.add(duplicate); } } } } - } - - private void getSourceCountryChains(List rios) { - Map> sourceCountryChains = new HashMap<>(); - for (var rio : rios) { - Integer outboundNodeId = rio.getOutboundNode().getId(); - - List chains = null; - - if (sourceCountryChains.containsKey(outboundNodeId)) { - chains = sourceCountryChains.get(outboundNodeId); - } else { - chains = chainResolver.buildChains(outboundNodeId).stream().map(ChainInformationObject::new).toList(); - sourceCountryChains.put(outboundNodeId, chains); - } - - rio.setSourceCountryChains(chains); - } + container.overrideRoutes(routes); } private Integer getRegionRadius() { @@ -279,391 +399,390 @@ public class RoutingService { return property.map(propertyDTO -> Integer.valueOf(propertyDTO.getCurrentValue())).orElseGet(SystemPropertyMappingId.RADIUS_REGION::getDefaultAsInteger); } - private ArrayList findRoutesWithDestinationChainsAndSource(List> chains, Node source, Node destination) { - ArrayList foundRoutes = new ArrayList<>(); + private void connectDestinationChainAndMainRun(TemporaryContainer container, List outboundNodes, List> destinationChains) { + /* + * Try to connect everything together: + * - go trough all main runs and adjacent post-runs + * - find any compatible chain: + * - check if chain is routable + * - add post run and main run + */ + for (var mainRun : container.getMainRuns()) { - HashMap matrixRates = new HashMap<>(); + Node mainRunEndNode = nodeRepository.getById(mainRun.getToNodeId()).orElseThrow(); + Node mainRunStartNode = outboundNodes.stream().filter(n -> n.getId().equals(mainRun.getFromNodeId())).findFirst().orElseThrow(); - for (var chain : chains) { - var destinationCountryId = destination.getCountryId(); - MatrixRate matrixRate = null; + TemporaryRateObject mainRunObj = new TemporaryRateObject(mainRunStartNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN, mainRun); - if (!chain.isEmpty()) { - destinationCountryId = chain.getLast().getCountryId(); - } + for (var postRun : container.getPostRuns().get(mainRun.getId())) { - if (!matrixRates.containsKey(destinationCountryId)) { - matrixRate = matrixRateRepository.getByCountryIds(source.getCountryId(), destinationCountryId).orElse(null); - matrixRates.put(destinationCountryId, matrixRate); + Node postRunEndNode = nodeRepository.getById(postRun.getToNodeId()).orElseThrow(); + TemporaryRateObject postRunObj = new TemporaryRateObject(postRunEndNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.POST_RUN, postRun); + + for (var chain : destinationChains) { + ChainQuality quality = getChainQuality(chain, postRun, mainRun); + + /* if connection quality is bad, do not try to route this and continue. */ + if (quality == ChainQuality.FALLBACK) continue; + + boolean routable = true; + TemporaryRouteObject routeObj = new TemporaryRouteObject(); + + for (int idx = 1; idx < chain.size() - quality.getSizeOffset(); idx++) { + Node startNode = chain.get(idx); + Node endNode = chain.get(idx - 1); + + var rate = connectNodes(startNode, endNode, container); + + if (rate != null) { + routeObj.addSection(rate); + } else { + // chain is not routable -> discard + routable = false; + break; + } + } + + if (routable) { + routeObj.addPostRunSection(postRunObj); + routeObj.addMainRunSection(mainRunObj); + container.addRoute(routeObj); + } + } } } - - for (var destinationCountryId : matrixRates.keySet()) { - var matrixRate = matrixRates.get(destinationCountryId); - var destinationCountryChains = chains.stream().filter(chain -> chain.getLast().getCountryId().equals(destinationCountryId)).map(ChainInformationObject::new).toList(); - foundRoutes.add(new RouteInformationObject(destination, source, matrixRate, destinationCountryChains)); - } - - return foundRoutes; } - private enum ChainConnectionQuality { + private ChainQuality getChainQuality(List chain, ContainerRate postRun, ContainerRate mainRun) { + if (chain.getLast().getId().equals(postRun.getToNodeId())) { + return ChainQuality.MEDIUM; + } else if (chain.getLast().getId().equals(postRun.getFromNodeId()) && chain.get(chain.size() - 2).getId().equals(postRun.getToNodeId())) { + return ChainQuality.HIGH; + } else if (chain.getLast().getCountryId().equals(postRun.getToCountryId())) { + return ChainQuality.LOW; + } + return ChainQuality.FALLBACK; + } + + private TemporaryRateObject connectNodes(Node startNode, Node endNode, TemporaryContainer container) { + var containerRateObj = new TemporaryRateObject(startNode, endNode, TemporaryRateObject.TemporaryRateObjectType.CONTAINER); + var matrixRateObj = new TemporaryRateObject(startNode, endNode, TemporaryRateObject.TemporaryRateObjectType.MATRIX); + + if (container.getRates().contains(containerRateObj)) + return container.getRates().stream().filter(r -> r.equals(containerRateObj)).findFirst().orElseThrow(); + + if (container.getRates().contains(matrixRateObj)) + return container.getRates().stream().filter(r -> r.equals(matrixRateObj)).findFirst().orElseThrow(); + + Optional containerRate = containerRateRepository.findRoute(startNode.getId(), endNode.getId(), ContainerRateType.ROAD); + + if (containerRate.isPresent()) { + containerRateObj.setRate(containerRate.get()); + container.getRates().add(containerRateObj); + return containerRateObj; + } else { + Optional matrixRate = matrixRateRepository.getByCountryIds(startNode.getCountryId(), endNode.getCountryId()); + + if (matrixRate.isPresent()) { + matrixRateObj.setRate(matrixRate.get()); + matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true)); + container.getRates().add(matrixRateObj); + return matrixRateObj; + } else { + return null; + } + } + } + + private enum ChainQuality { HIGH(1), MEDIUM(0), LOW(0), FALLBACK(0); - private final int startIdx; + private final int sizeOffset; - ChainConnectionQuality(int startIdx) { - this.startIdx = startIdx; + ChainQuality(int sizeOffset) { + this.sizeOffset = sizeOffset; } - public int getStartIdx() { - return startIdx; + public int getSizeOffset() { + return sizeOffset; } } - private interface ChainSectionRate { + private static class TemporaryContainer { - BigDecimal getCost(); - - int getLeadTime(); - - } - - private static class ChainInformationObject { - - private final ChainConnectionQuality quality; - private final List chain; - private final Collection sections; - private Node nearByNode; - - public ChainInformationObject() { - this.chain = new ArrayList<>(); - this.nearByNode = null; - this.sections = new ArrayList<>(); - this.quality = ChainConnectionQuality.FALLBACK; - } - - public ChainInformationObject(List chain, ChainConnectionQuality quality) { - this.chain = chain; - this.nearByNode = null; - this.sections = new ArrayList<>(); - this.quality = quality; - } - - public ChainInformationObject(List chain) { - this.chain = chain; - this.nearByNode = null; - this.sections = new ArrayList<>(); - this.quality = ChainConnectionQuality.FALLBACK; - } - - public List getChain() { - return chain; - } - - public void addNearByNode(Node nearByNode) { - this.nearByNode = nearByNode; - } - - public void addSectionInformation(ChainSectionInformation section) { - this.sections.add(section); - } - - public double getCost() { - return sections.stream().mapToDouble(ChainSectionInformation::getCost).sum(); - } - - public int getLeadTime() { - return sections.stream().mapToInt(ChainSectionInformation::getLeadTime).sum(); - } - - public ChainConnectionQuality getQuality() { - return quality; - } - - public Collection getSections() { - return sections; - } - } - - private static class ChainSectionInformation { - - Node fromNode; - Node toNode; - - ChainSectionRate rate; - - public ChainSectionInformation(Node fromNode, Node toNode, ChainSectionRate rate) { - this.fromNode = fromNode; - this.toNode = toNode; - this.rate = rate; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - ChainSectionInformation that = (ChainSectionInformation) o; - return Objects.equals(fromNode, that.fromNode) && Objects.equals(toNode, that.toNode); - } - - @Override - public int hashCode() { - return Objects.hash(fromNode, toNode); - } - - public double getCost() { - return rate.getCost().doubleValue(); - } - - public int getLeadTime() { - return rate.getLeadTime(); - } - } - - private static class ChainSectionContainerRateInformation implements ChainSectionRate { - Node fromNode; - Node toNode; - - ContainerRate rate; - - public ChainSectionContainerRateInformation(Node fromNode, Node toNode) { - this.fromNode = fromNode; - this.toNode = toNode; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - ChainSectionContainerRateInformation that = (ChainSectionContainerRateInformation) o; - - return Objects.equals(fromNode.getId(), that.fromNode.getId()) && Objects.equals(toNode.getId(), that.toNode.getId()); - } - - @Override - public int hashCode() { - return Objects.hash(fromNode.getId(), toNode.getId()); - } - - public void setRate(ContainerRate containerRate) { - this.rate = containerRate; - } - - @Override - public BigDecimal getCost() { - return rate.getRateFeu(); - } - - @Override - public int getLeadTime() { - return rate.getLeadTime(); - } - - } - - private static class ChainSectionMatrixRateInformation implements ChainSectionRate { - Integer fromCountryId; - Integer toCountryId; - - MatrixRate rate; - private int distance; - - - public ChainSectionMatrixRateInformation(Integer fromCountryId, Integer toCountryId) { - this.fromCountryId = fromCountryId; - this.toCountryId = toCountryId; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - ChainSectionMatrixRateInformation that = (ChainSectionMatrixRateInformation) o; - - return Objects.equals(fromCountryId, that.fromCountryId) && Objects.equals(toCountryId, that.toCountryId); - } - - @Override - public int hashCode() { - return Objects.hash(fromCountryId, toCountryId); - } - - public void setRate(MatrixRate matrixRate) { - this.rate = matrixRate; - } - - - void setDistance(int distance) { - this.distance = distance; - } - - @Override - public BigDecimal getCost() { - return rate.getRate().multiply(BigDecimal.valueOf(distance)); - } - - @Override - public int getLeadTime() { - return 3; - } - } - - private static class RouteInformationObject { - - /** - * If a destination chain can be connected directly with a matrix rate to the source node. - * The matrix rate is stored in matrixRate. The destinationChains only contains one chain in this case. - * mainRun, outboundNode and inboundNode are null then and postRuns is empty. + /* + * Set to lookup route sections. Generated from node pairs. */ - private final MatrixRate matrixRate; - /** - * If a destination chain cannot be connected directly with a matrix rate to the source node. - * The matrix rate is null and mainRun and postRuns are set. + private final Set rates = new HashSet<>(); + /* + * Routes that are build within the routing service. */ + private final Collection routes = new ArrayList<>(); + /* + * Source and destination node + */ + private final Node source; + private final Node destination; + /* + * mainRuns and postRuns retrieved from database. + */ + private Map> mainRuns; + private Map> postRuns; + private MatrixRate sourceMatrixRate; - private final ContainerRate mainRun; - private final List postRuns; - private Node destinationNode; - private Node sourceNode; - private boolean isUserNode; - private Node outboundNode; - private Node inboundNode; - - - private List destinationChains = new ArrayList<>(); - private List sourceCountryChains; - - public RouteInformationObject(Node destination, Node source, ContainerRate mainRun, Node outboundNode, Node inboundNode, List postRuns) { - this.mainRun = mainRun; - this.outboundNode = outboundNode; - this.inboundNode = inboundNode; - this.postRuns = postRuns; - this.matrixRate = null; - this.sourceNode = source; - this.destinationNode = destination; - } - - public RouteInformationObject(Node destination, Node source, MatrixRate matrixRate, List destinationCountryChains) { - this.matrixRate = matrixRate; - this.mainRun = null; - this.outboundNode = null; - this.postRuns = Collections.emptyList(); - - this.destinationChains.addAll(destinationCountryChains); - this.sourceCountryChains = Collections.singletonList(new ChainInformationObject()); - - this.destinationNode = destination; - this.sourceNode = source; + public TemporaryContainer(Node source, Node destination) { + this.source = source; + this.destination = destination; + this.mainRuns = null; + this.postRuns = null; + this.sourceMatrixRate = null; } - public List getPostRuns() { + public Collection getRates() { + return rates; + } + + public List getMainRuns(Integer outboundNodeId) { + return mainRuns.get(outboundNodeId); + } + + public List getMainRuns() { + return mainRuns.values().stream().flatMap(Collection::stream).toList(); + } + + public void setMainRuns(Map> mainRuns) { + this.mainRuns = mainRuns; + } + + public Map> getPostRuns() { return postRuns; } - public void addDestinationChain(ChainInformationObject chain) { - destinationChains.add(chain); + public void setPostRuns(Map> postRuns) { + this.postRuns = postRuns; } - public Node getOutboundNode() { - return outboundNode; + public void addRoute(TemporaryRouteObject route) { + this.routes.add(route); } - public List getDestinationChains() { - return destinationChains; + public Collection getRoutes() { + return routes; } - public List getSourceCountryChains() { - return sourceCountryChains; + public Node getSourceNode() { + return source; } - public void setSourceCountryChains(List sourceCountryChains) { - this.sourceCountryChains = sourceCountryChains; + public Node getDestinationNode() { + return destination; } - public void removeDestinationChain(ChainInformationObject chain) { - destinationChains.remove(chain); + public void setSourceMatrixRate(MatrixRate sourceMatrixRate) { + this.sourceMatrixRate = sourceMatrixRate; } - public void removeSourceChain(ChainInformationObject chain) { - sourceCountryChains.remove(chain); + public boolean hasSourceMatrixRate() { + return sourceMatrixRate != null; } - public RouteInformation getRouteInformation(Set containerRates, Set matrixRates) { + public void overrideRoutes(Collection routes) { + this.routes.clear(); + this.routes.addAll(routes); + } + } - var destinationChain = destinationChains.stream().min(Comparator.comparing(ChainInformationObject::getCost)); - if (destinationChain.isEmpty()) return null; + private static class TemporaryRouteObject { - var nodes = new ArrayList(); - var sections = new ArrayList(); + private final List sections; - RouteInformation routeInformation = new RouteInformation(); - routeInformation.setRoute(new Route()); - routeInformation.setRouteNodes(nodes); - routeInformation.setRouteSections(sections); + private TemporaryRateObject mainRun; + private TemporaryRateObject postRun; - if (matrixRate == null) { - var sourceChain = sourceCountryChains.stream().min(Comparator.comparing(ChainInformationObject::getCost)); - if (sourceChain.isEmpty()) return null; + private boolean nearBy = false; + private boolean isCheapest = false; + private boolean isFastest = false; - nodes.add(mapNode(sourceNode, isUserNode)); - nodes.addAll(sourceChain.get().getChain().reversed().stream().map(n -> mapNode(n, false)).toList()); - - nodes.add(mapNode(outboundNode, false)); - nodes.add(mapNode(inboundNode, false)); - - sections.add(new ChainSectionInformation(sourceChain.get().getChain().getFirst(), sourceNode, )) - sections.addAll(sourceChain.get().getSections().stream().map(this::mapSection).toList()); - - } - - nodes.addAll(destinationChain.get().getChain().stream().map(n -> mapNode(n, false)).toList()); - - - return routeInformation; + public TemporaryRouteObject() { + sections = new ArrayList<>(); } - private RouteSection mapSection(ChainSectionInformation s) { - - RouteSection section = new RouteSection(); - - section.set - - return section; + public List getSections() { + return sections; } - private RouteNode mapNode(Node node, boolean isUserNode) { - RouteNode routeNode = new RouteNode(); - - routeNode.setNodeId(!isUserNode ? node.getId() : null); - routeNode.setUserNodeId(isUserNode ? node.getId() : null); - routeNode.setCountryId(node.getCountryId()); - routeNode.setGeoLat(node.getGeoLat()); - routeNode.setGeoLng(node.getGeoLng()); - routeNode.setName(node.getName()); - routeNode.setAddress(node.getAddress()); - routeNode.setOutdated(node.getDeprecated()); - - routeNode.setIntermediate(false); - routeNode.setDestination(false); - routeNode.setSource(false); - - return routeNode; + public void addSection(TemporaryRateObject section) { + this.sections.add(section); } - - public void setSource(Node node, boolean isUserNode) { - this.sourceNode = node; - this.isUserNode = isUserNode; + public void addMainRunSection(TemporaryRateObject mainRun) { + addSection(mainRun); + this.mainRun = mainRun; } - public ContainerRate getMainRun() { + public void addPostRunSection(TemporaryRateObject postRun) { + addSection(postRun); + this.postRun = postRun; + } + + public TemporaryRateObject getMainRun() { return mainRun; } - public void setInboundNode(Node node) { - this.inboundNode = node; + public void setMainRun(TemporaryRateObject mainRun) { + this.mainRun = mainRun; + } + + public TemporaryRateObject getPostRun() { + return postRun; + } + + @Override + public TemporaryRouteObject clone() { + TemporaryRouteObject clone = new TemporaryRouteObject(); + clone.sections.addAll(sections); + clone.mainRun = mainRun; + clone.postRun = postRun; + clone.nearBy = nearBy; + clone.isCheapest = isCheapest; + clone.isFastest = isFastest; + return clone; + } + + public void routeOverNearBy() { + this.nearBy = true; + } + + public double getCost() { + return sections.stream().mapToDouble(TemporaryRateObject::getCost).sum(); + } + + public void setCheapest() { + this.isCheapest = true; + } + + public void setFastest() { + this.isFastest = true; + } + + public int getLeadTime() { + return sections.stream().mapToInt(TemporaryRateObject::getLeadTime).sum(); + + } + + public Boolean isCheapest() { + return isCheapest; + } + + public Boolean isFastest() { + return isFastest; } } + private static class TemporaryRateObject { + + private final Node fromNode; + private final Node toNode; + private MatrixRate matrixRate; + private double approxDistance; + private ContainerRate containerRate; + + private TemporaryRateObjectType type; + + + public TemporaryRateObject(Node fromNode, Node toNode, TemporaryRateObjectType type) { + + this.fromNode = fromNode; + this.toNode = toNode; + this.type = type; + } + + public TemporaryRateObject(Node fromNode, Node toNode, TemporaryRateObjectType type, ContainerRate rate) { + this.fromNode = fromNode; + this.toNode = toNode; + this.type = type; + this.containerRate = rate; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TemporaryRateObject that = (TemporaryRateObject) o; + + if (this.type.equals(that.type)) { + if (this.type.equals(TemporaryRateObjectType.MATRIX)) { + return Objects.equals(this.fromNode.getCountryId(), that.fromNode.getCountryId()) && Objects.equals(this.toNode.getCountryId(), that.toNode.getCountryId()); + } else if (this.type.equals(TemporaryRateObjectType.CONTAINER) || this.type.equals(TemporaryRateObjectType.MAIN_RUN) || this.type.equals(TemporaryRateObjectType.POST_RUN)) { + return Objects.equals(this.fromNode.getId(), that.fromNode.getId()) && Objects.equals(this.toNode.getId(), that.toNode.getId()); + } + } + + return false; + } + + @Override + public int hashCode() { + if (matrixRate != null) return Objects.hash(matrixRate.getFromCountry(), matrixRate.getToCountry()); + + if (containerRate != null) return Objects.hash(containerRate.getFromNodeId(), containerRate.getToNodeId()); + + return Objects.hash(null, null); + } + + public void setRate(ContainerRate containerRate) { + this.containerRate = containerRate; + this.type = TemporaryRateObjectType.CONTAINER; + } + + public void setRate(MatrixRate matrixRate) { + this.matrixRate = matrixRate; + this.type = TemporaryRateObjectType.MATRIX; + } + + public double getApproxDistance() { + return approxDistance; + } + + public void setApproxDistance(double distance) { + this.approxDistance = distance; + } + + public Node getFromNode() { + return fromNode; + } + + public Node getToNode() { + return toNode; + } + + public double getCost() { + if(type.equals(TemporaryRateObjectType.MATRIX)) { + return matrixRate.getRate().doubleValue() * approxDistance; + } else { + return containerRate.getRateFeu().doubleValue(); + } + } + + public int getLeadTime() { + if (type.equals(TemporaryRateObjectType.MATRIX)) { + return 3; + } + return containerRate.getLeadTime(); + } + + public TemporaryRateObjectType getType() { + return type; + } + + public ContainerRateType getContainerRateTye() { + return containerRate.getType(); + } + + private enum TemporaryRateObjectType { + MATRIX, CONTAINER, POST_RUN, MAIN_RUN; + } + } } diff --git a/src/main/java/de/avatic/lcc/service/calculation/RoutingService2.java b/src/main/java/de/avatic/lcc/service/calculation/RoutingService2.java deleted file mode 100644 index 0d7df6e..0000000 --- a/src/main/java/de/avatic/lcc/service/calculation/RoutingService2.java +++ /dev/null @@ -1,796 +0,0 @@ -package de.avatic.lcc.service.calculation; - -import de.avatic.lcc.dto.generic.RouteType; -import de.avatic.lcc.model.nodes.Node; -import de.avatic.lcc.model.premises.route.Route; -import de.avatic.lcc.model.premises.route.RouteInformation; -import de.avatic.lcc.model.premises.route.RouteNode; -import de.avatic.lcc.model.premises.route.RouteSection; -import de.avatic.lcc.model.properties.SystemPropertyMappingId; -import de.avatic.lcc.model.rates.ContainerRate; -import de.avatic.lcc.model.rates.ContainerRateType; -import de.avatic.lcc.model.rates.MatrixRate; -import de.avatic.lcc.repositories.NodeRepository; -import de.avatic.lcc.repositories.properties.PropertyRepository; -import de.avatic.lcc.repositories.rates.ContainerRateRepository; -import de.avatic.lcc.repositories.rates.MatrixRateRepository; -import org.springframework.stereotype.Service; - -import java.util.*; -import java.util.stream.Collectors; - -@Service -public class RoutingService2 { - - - private final MatrixRateRepository matrixRateRepository; - private final ChainResolver chainResolver; - private final NodeRepository nodeRepository; - private final ContainerRateRepository containerRateRepository; - private final DistanceService distanceService; - private final PropertyRepository propertyRepository; - - public RoutingService2(MatrixRateRepository matrixRateRepository, ChainResolver chainResolver, NodeRepository nodeRepository, ContainerRateRepository containerRateRepository, DistanceService distanceService, PropertyRepository propertyRepository) { - this.matrixRateRepository = matrixRateRepository; - this.chainResolver = chainResolver; - this.nodeRepository = nodeRepository; - this.containerRateRepository = containerRateRepository; - this.distanceService = distanceService; - this.propertyRepository = propertyRepository; - } - - public List findRoute(Node destination, Node source, boolean sourceIsUserNode) { - List routeInformation = new ArrayList<>(); - TemporaryContainer container = new TemporaryContainer(source, destination); - - /* - * Get the source and destination node from database. - * Check if there is a matrix rate for the source country. - */ - matrixRateRepository.getByCountryIds(source.getCountryId(), source.getCountryId()).ifPresent(container::setSourceMatrixRate); - - - /* - * Generate recursive all chains starting with the destination node. - * This means all chains within the last node of a chain are connected to the - * existing chain. - * - * Furthermore, it is evaluated that all nodes within the chain do not have chains - * themselves that are in conflict with the chain. - * - * Then get all countries from the end of the destination chains. - */ - - List> destinationChains = chainResolver.buildChains(destination.getId()); - List inboundCountries = destinationChains.stream().filter(chain -> !chain.isEmpty()).map(chain -> chain.getLast().getCountryId()).distinct().toList(); - - /* - * Get all outbound nodes for the country of the source node. In this first step this includes: - * - all intermediate nodes that have the same country id. - * - all nodes that are explicitly mapped as outbound node for this country id. - */ - List outboundNodes = nodeRepository.getAllOutboundFor(source.getCountryId()); - - /* - * Find main runs based on the outbound nodes and the inbound countries found before. - * Find post-runs for the main runs. - * - * Store all information in the TemporaryContainer object. - */ - container.setMainRuns(outboundNodes.stream().collect(Collectors.toMap(Node::getId, n -> containerRateRepository.findRoutesByStartNodeIdAndDestinationCountryId(n.getId(), inboundCountries)))); - container.setPostRuns(container.getMainRuns().stream().collect(Collectors.toMap(ContainerRate::getId, containerRateRepository::getPostRunsFor))); - - - connectDestinationChainAndMainRun(container, outboundNodes, destinationChains); - connectSourceChainAndSource(container); - - /* - * At this point all routes with a main run are created. - * We find now the best route per main run and throw away the rest. - */ - findCheapestPerMainRun(container); - - /* - * Now we also create routes without main run (matrix rate) - */ - connectDestinationChainDirectly(container, destinationChains); - - /* - * finally find and mark the fastest and the cheapest route. - */ - findAndMarkCheapestAndFastest(container); - - /* - * Convert to Database model - */ - for (var route : container.getRoutes()) { - RouteInformation routeInformationObj = new RouteInformation(); - - routeInformationObj.setRoute(mapRoute(route)); - routeInformationObj.setRouteSections(mapSections(route.getSections())); - routeInformationObj.setRouteNodes(route.getNodes().stream().map(n -> mapNode(n, n.getId().equals(source.getId()) && sourceIsUserNode)).toList()); - routeInformation.add(routeInformationObj); - } - - return routeInformation; - } - - - private RouteNode mapNode(Node node, boolean isUserNode) { - RouteNode routeNode = new RouteNode(); - - routeNode.setName(node.getName()); - routeNode.setCountryId(node.getCountryId()); - routeNode.setAddress(node.getAddress()); - routeNode.setGeoLng(node.getGeoLng()); - routeNode.setGeoLat(node.getGeoLat()); - routeNode.setUserNodeId(isUserNode? node.getId() : null); - routeNode.setNodeId(isUserNode ? null : node.getId()); - routeNode.setIntermediate(node.getIntermediate() != null ? node.getIntermediate() : false); - routeNode.setDestination(node.getDestination() != null ? node.getIntermediate() : false); - routeNode.setSource(node.getSource() != null ? node.getIntermediate() : false); - routeNode.setOutdated(node.getDeprecated()); - - return routeNode; - } - - private Route mapRoute(TemporaryRouteObject route) { - Route routeObj = new Route(); - - routeObj.setCheapest(route.isCheapest()); - routeObj.setFastest(route.isFastest()); - - return routeObj; - } - - private List mapSections(List sections) { - int index = 1; - - List routeSections = new ArrayList<>(); - boolean passedMainRun = false; - - for(var section : sections) { - var routeSection = mapSection(section); - - if (!section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN) && !section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN)) { - if (!passedMainRun) - routeSection.setPreRun(true); - - if (passedMainRun) - routeSection.setPostRun(true); - - } - - if(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN) || section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN)) - passedMainRun = true; - - routeSection.setListPosition(index++); - routeSections.add(routeSection); - } - - return routeSections; - } - - private RouteSection mapSection(TemporaryRateObject section) { - RouteSection routeSection = new RouteSection(); - - routeSection.setTransportType(mapRouteType(section)); - routeSection.setFromRouteNodeId(section.getFromNode().getId()); - routeSection.setToRouteNodeId(section.getToNode().getId()); - routeSection.setMainRun(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN)); - routeSection.setOutdated(false); - - routeSection.setPostRun(false); - routeSection.setPreRun(false); - - return routeSection; - - } - - private RouteType mapRouteType(TemporaryRateObject rate) { - - switch(rate.getType()) { - case MATRIX, CONTAINER -> { - return RouteType.ROAD; - } - case POST_RUN -> { - return RouteType.POST_RUN; - } - case MAIN_RUN -> { - return RouteType.valueOf(rate.getContainerRateTye().name()); - } - } - - return null; - } - - private void findAndMarkCheapestAndFastest(TemporaryContainer container) { - var routes = container.getRoutes(); - - TemporaryRouteObject cheapest = null; - double cheapestCost = Double.MAX_VALUE; - - TemporaryRouteObject fastest = null; - double fastestRoute = Double.MAX_VALUE; - - for(var route : routes) { - double routeCost = route.getCost(); - int leadTime = route.getLeadTime(); - - if(routeCost < cheapestCost) { - cheapestCost = routeCost; - cheapest = route; - } - - if(leadTime < fastestRoute) { - fastestRoute = leadTime; - fastest = route; - } - } - - if(cheapest != null) { - cheapest.setCheapest(); - } - - if(fastest != null) { - fastest.setFastest(); - } - } - - private void findCheapestPerMainRun(TemporaryContainer container) { - - var routesByMainRun = container.getRoutes().stream().collect(Collectors.groupingBy(TemporaryRouteObject::getMainRun)); - Collection cheapestRoutes = new ArrayList<>(); - - for(var mainRun : routesByMainRun.keySet()) { - List routes = routesByMainRun.get(mainRun); - TemporaryRouteObject cheapest = null; - double cheapestCost = Double.MAX_VALUE; - - for(var route : routes) { - double routeCost = route.getCost(); - - if(routeCost < cheapestCost) { - cheapestCost = routeCost; - cheapest = route; - } - } - - if(cheapest != null) { - cheapestRoutes.add(cheapest); - } - } - - container.overrideRoutes(cheapestRoutes); - } - - private void connectDestinationChainDirectly(TemporaryContainer container, List> chains) { - Collection rates = container.getRates(); - - for (var chain : chains) { - var toNode = chain.isEmpty() ? container.getDestinationNode() : chain.getLast(); - - TemporaryRateObject finalSection = new TemporaryRateObject(container.getSourceNode(), toNode, TemporaryRateObject.TemporaryRateObjectType.MATRIX); - - if (rates.contains(finalSection)) { - TemporaryRateObject finalMatrixRateObj = finalSection; - finalSection = container.getRates().stream().filter(r -> r.equals(finalMatrixRateObj)).findFirst().orElse(null); - } else { - var matrixRate = matrixRateRepository.getByCountryIds(container.getSourceNode().getCountryId(), toNode.getCountryId()).orElse(null); - - // no matrix rate in database. skip this chain - if (matrixRate == null) { - continue; - } - - finalSection.setRate(matrixRate); - finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true)); - rates.add(finalSection); - } - - // could not create an temporary rate object -> so just skip here ... (should not happen) - if (finalSection == null) { - continue; - } - - // create a route. - boolean routable = true; - TemporaryRouteObject routeObj = new TemporaryRouteObject(); - - for (int idx = 1; idx < chain.size(); idx++) { - Node startNode = chain.get(idx); - Node endNode = chain.get(idx - 1); - - var rate = connectNodes(startNode, endNode, container); - - if (rate != null) { - routeObj.addSection(rate); - } else { - // chain is not routable -> discard - routable = false; - break; - } - } - - // if the chain is routable -> add the final rate and save the route. - if (routable) { - routeObj.addSection(finalSection); - container.addRoute(routeObj); - } - - } - } - - - private TemporaryRateObject connectNearByNodes(Node chainEnd, List nearByNodes, TemporaryContainer container) { - - - for (var nearbyNode : nearByNodes) { - TemporaryRateObject nearByRate = connectNodes(nearbyNode, chainEnd, container); - if (null != nearByRate) { - return nearByRate; - } - } - return null; - } - - private void connectSourceChainAndSource(TemporaryContainer container) { - - /* get the near-by nodes if no country matrix rate present */ - List nearByNodes = (container.hasSourceMatrixRate()) ? null : nodeRepository.getByDistance(container.getSourceNode(), getRegionRadius()); - - Collection routes = new ArrayList<>(); - - for (var route : container.getRoutes()) { - var mainRun = route.getMainRun(); - var sourceChains = chainResolver.buildChains(mainRun.getFromNode().getId()); - - - for (var chain : sourceChains) { - Node source = container.getSourceNode(); - boolean chainEndIsSource = source.getId().equals(chain.getLast().getId()); - - // find final section: check if chain end and source node are identical, then check if chain end can be connected to - // source node, if this is not possible use a near-by node - TemporaryRateObject finalSection = (chainEndIsSource) ? null : connectNodes(source, chain.getLast(), container); - finalSection = ((finalSection == null && !chainEndIsSource && nearByNodes != null) ? connectNearByNodes(chain.getLast(), nearByNodes, container) : finalSection); - - if (finalSection != null || chainEndIsSource) { - boolean routable = true; - TemporaryRouteObject duplicate = route.clone(); - - for (int idx = 1; idx < chain.size() - 1; idx++) { - Node startNode = chain.get(idx); - Node endNode = chain.get(idx - 1); - - TemporaryRateObject rate = connectNodes(startNode, endNode, container); - - if (rate != null) duplicate.addSection(rate); - else { - routable = false; - break; - } - } - - if (routable) { - if (finalSection != null) { - // add final section if necessary, - // if last chain node == source node this can be skipped. - duplicate.addSection(finalSection); - if (!finalSection.getFromNode().getId().equals(source.getId())) duplicate.routeOverNearBy(); - } - routes.add(duplicate); - } - } - } - } - container.overrideRoutes(routes); - } - - private Integer getRegionRadius() { - var property = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.RADIUS_REGION); - return property.map(propertyDTO -> Integer.valueOf(propertyDTO.getCurrentValue())).orElseGet(SystemPropertyMappingId.RADIUS_REGION::getDefaultAsInteger); - } - - private void connectDestinationChainAndMainRun(TemporaryContainer container, List outboundNodes, List> destinationChains) { - /* - * Try to connect everything together: - * - go trough all main runs and adjacent post-runs - * - find any compatible chain: - * - check if chain is routable - * - add post run and main run - */ - for (var mainRun : container.getMainRuns()) { - - Node mainRunEndNode = nodeRepository.getById(mainRun.getToNodeId()).orElseThrow(); - Node mainRunStartNode = outboundNodes.stream().filter(n -> n.getId().equals(mainRun.getFromNodeId())).findFirst().orElseThrow(); - - TemporaryRateObject mainRunObj = new TemporaryRateObject(mainRunStartNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN, mainRun); - - for (var postRun : container.getPostRuns().get(mainRun.getId())) { - - Node postRunEndNode = nodeRepository.getById(postRun.getToNodeId()).orElseThrow(); - TemporaryRateObject postRunObj = new TemporaryRateObject(postRunEndNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.POST_RUN, postRun); - - for (var chain : destinationChains) { - ChainQuality quality = getChainQuality(chain, postRun, mainRun); - - /* if connection quality is bad, do not try to route this and continue. */ - if (quality == ChainQuality.FALLBACK) continue; - - boolean routable = true; - TemporaryRouteObject routeObj = new TemporaryRouteObject(); - - for (int idx = 1; idx < chain.size() - quality.getSizeOffset(); idx++) { - Node startNode = chain.get(idx); - Node endNode = chain.get(idx - 1); - - var rate = connectNodes(startNode, endNode, container); - - if (rate != null) { - routeObj.addSection(rate); - } else { - // chain is not routable -> discard - routable = false; - break; - } - } - - if (routable) { - routeObj.addPostRunSection(postRunObj); - routeObj.addMainRunSection(mainRunObj); - container.addRoute(routeObj); - } - } - } - } - } - - - private ChainQuality getChainQuality(List chain, ContainerRate postRun, ContainerRate mainRun) { - if (chain.getLast().getId().equals(postRun.getToNodeId())) { - return ChainQuality.MEDIUM; - } else if (chain.getLast().getId().equals(postRun.getFromNodeId()) && chain.get(chain.size() - 2).getId().equals(postRun.getToNodeId())) { - return ChainQuality.HIGH; - } else if (chain.getLast().getCountryId().equals(postRun.getToCountryId())) { - return ChainQuality.LOW; - } - return ChainQuality.FALLBACK; - } - - private TemporaryRateObject connectNodes(Node startNode, Node endNode, TemporaryContainer container) { - var containerRateObj = new TemporaryRateObject(startNode, endNode, TemporaryRateObject.TemporaryRateObjectType.CONTAINER); - var matrixRateObj = new TemporaryRateObject(startNode, endNode, TemporaryRateObject.TemporaryRateObjectType.MATRIX); - - if (container.getRates().contains(containerRateObj)) - return container.getRates().stream().filter(r -> r.equals(containerRateObj)).findFirst().orElseThrow(); - - if (container.getRates().contains(matrixRateObj)) - return container.getRates().stream().filter(r -> r.equals(matrixRateObj)).findFirst().orElseThrow(); - - Optional containerRate = containerRateRepository.findRoute(startNode.getId(), endNode.getId(), ContainerRateType.ROAD); - - if (containerRate.isPresent()) { - containerRateObj.setRate(containerRate.get()); - container.getRates().add(containerRateObj); - return containerRateObj; - } else { - Optional matrixRate = matrixRateRepository.getByCountryIds(startNode.getCountryId(), endNode.getCountryId()); - - if (matrixRate.isPresent()) { - matrixRateObj.setRate(matrixRate.get()); - matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true)); - container.getRates().add(matrixRateObj); - return matrixRateObj; - } else { - return null; - } - } - } - - private enum ChainQuality { - HIGH(1), MEDIUM(0), LOW(0), FALLBACK(0); - - private final int sizeOffset; - - ChainQuality(int sizeOffset) { - this.sizeOffset = sizeOffset; - } - - public int getSizeOffset() { - return sizeOffset; - } - } - - private static class TemporaryContainer { - - /* - * Set to lookup route sections. Generated from node pairs. - */ - private final Set rates = new HashSet<>(); - /* - * Routes that are build within the routing service. - */ - private final Collection routes = new ArrayList<>(); - /* - * Source and destination node - */ - private final Node source; - private final Node destination; - /* - * mainRuns and postRuns retrieved from database. - */ - private Map> mainRuns; - private Map> postRuns; - private MatrixRate sourceMatrixRate; - - public TemporaryContainer(Node source, Node destination) { - this.source = source; - this.destination = destination; - this.mainRuns = null; - this.postRuns = null; - this.sourceMatrixRate = null; - } - - - public Collection getRates() { - return rates; - } - - public List getMainRuns(Integer outboundNodeId) { - return mainRuns.get(outboundNodeId); - } - - public List getMainRuns() { - return mainRuns.values().stream().flatMap(Collection::stream).toList(); - } - - public void setMainRuns(Map> mainRuns) { - this.mainRuns = mainRuns; - } - - public Map> getPostRuns() { - return postRuns; - } - - public void setPostRuns(Map> postRuns) { - this.postRuns = postRuns; - } - - public void addRoute(TemporaryRouteObject route) { - this.routes.add(route); - } - - public Collection getRoutes() { - return routes; - } - - public Node getSourceNode() { - return source; - } - - public Node getDestinationNode() { - return destination; - } - - public void setSourceMatrixRate(MatrixRate sourceMatrixRate) { - this.sourceMatrixRate = sourceMatrixRate; - } - - public boolean hasSourceMatrixRate() { - return sourceMatrixRate != null; - } - - public void overrideRoutes(Collection routes) { - this.routes.clear(); - this.routes.addAll(routes); - } - } - - private static class TemporaryRouteObject { - - private final List sections; - - private TemporaryRateObject mainRun; - private TemporaryRateObject postRun; - - private boolean nearBy = false; - private boolean isCheapest = false; - private boolean isFastest = false; - - public TemporaryRouteObject() { - sections = new ArrayList<>(); - } - - public List getSections() { - return sections; - } - - public void addSection(TemporaryRateObject section) { - this.sections.add(section); - } - - public void addMainRunSection(TemporaryRateObject mainRun) { - addSection(mainRun); - this.mainRun = mainRun; - } - - public void addPostRunSection(TemporaryRateObject postRun) { - addSection(postRun); - this.postRun = postRun; - } - - public TemporaryRateObject getMainRun() { - return mainRun; - } - - public void setMainRun(TemporaryRateObject mainRun) { - this.mainRun = mainRun; - } - - public TemporaryRateObject getPostRun() { - return postRun; - } - - @Override - public TemporaryRouteObject clone() { - TemporaryRouteObject clone = new TemporaryRouteObject(); - clone.sections.addAll(sections); - clone.mainRun = mainRun; - clone.postRun = postRun; - clone.nearBy = nearBy; - clone.isCheapest = isCheapest; - clone.isFastest = isFastest; - return clone; - } - - public void routeOverNearBy() { - this.nearBy = true; - } - - public double getCost() { - return sections.stream().mapToDouble(TemporaryRateObject::getCost).sum(); - } - - public void setCheapest() { - this.isCheapest = true; - } - - public void setFastest() { - this.isFastest = true; - } - - public int getLeadTime() { - return sections.stream().mapToInt(TemporaryRateObject::getLeadTime).sum(); - - } - - public List getNodes() { - List nodes = new ArrayList<>(); - - for(var section : sections.reversed()) { - - if(sections.getFirst().equals(section)) - nodes.add(section.getFromNode()); - - nodes.add(section.getToNode()); - - } - - return nodes; - } - - public Boolean isCheapest() { - return isCheapest; - } - - public Boolean isFastest() { - return isFastest; - } - } - - - private static class TemporaryRateObject { - - private final Node fromNode; - private final Node toNode; - private MatrixRate matrixRate; - private double approxDistance; - private ContainerRate containerRate; - - private TemporaryRateObjectType type; - - - public TemporaryRateObject(Node fromNode, Node toNode, TemporaryRateObjectType type) { - - this.fromNode = fromNode; - this.toNode = toNode; - this.type = type; - } - - public TemporaryRateObject(Node fromNode, Node toNode, TemporaryRateObjectType type, ContainerRate rate) { - this.fromNode = fromNode; - this.toNode = toNode; - this.type = type; - this.containerRate = rate; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - TemporaryRateObject that = (TemporaryRateObject) o; - - if (this.type.equals(that.type)) { - if (this.type.equals(TemporaryRateObjectType.MATRIX)) { - return Objects.equals(this.fromNode.getCountryId(), that.fromNode.getCountryId()) && Objects.equals(this.toNode.getCountryId(), that.toNode.getCountryId()); - } else if (this.type.equals(TemporaryRateObjectType.CONTAINER) || this.type.equals(TemporaryRateObjectType.MAIN_RUN) || this.type.equals(TemporaryRateObjectType.POST_RUN)) { - return Objects.equals(this.fromNode.getId(), that.fromNode.getId()) && Objects.equals(this.toNode.getId(), that.toNode.getId()); - } - } - - return false; - } - - @Override - public int hashCode() { - if (matrixRate != null) return Objects.hash(matrixRate.getFromCountry(), matrixRate.getToCountry()); - - if (containerRate != null) return Objects.hash(containerRate.getFromNodeId(), containerRate.getToNodeId()); - - return Objects.hash(null, null); - } - - public void setRate(ContainerRate containerRate) { - this.containerRate = containerRate; - this.type = TemporaryRateObjectType.CONTAINER; - } - - public void setRate(MatrixRate matrixRate) { - this.matrixRate = matrixRate; - this.type = TemporaryRateObjectType.MATRIX; - } - - public double getApproxDistance() { - return approxDistance; - } - - public void setApproxDistance(double distance) { - this.approxDistance = distance; - } - - public Node getFromNode() { - return fromNode; - } - - public Node getToNode() { - return toNode; - } - - public double getCost() { - if(type.equals(TemporaryRateObjectType.MATRIX)) { - return matrixRate.getRate().doubleValue() * approxDistance; - } else { - return containerRate.getRateFeu().doubleValue(); - } - } - - public int getLeadTime() { - if (type.equals(TemporaryRateObjectType.MATRIX)) { - return 3; - } - return containerRate.getLeadTime(); - } - - public TemporaryRateObjectType getType() { - return type; - } - - public ContainerRateType getContainerRateTye() { - return containerRate.getType(); - } - - private enum TemporaryRateObjectType { - MATRIX, CONTAINER, POST_RUN, MAIN_RUN; - } - } -} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index fbe3601..8a219e4 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -373,6 +373,8 @@ CREATE TABLE IF NOT EXISTS premise_destination 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, repacking_cost DECIMAL(15, 2) DEFAULT NULL, handling_cost DECIMAL(15, 2) DEFAULT NULL, disposal_cost DECIMAL(15, 2) DEFAULT NULL,