From 3aa86b4eea76b8a979933ef93c39faf9a4eb367a Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 12 Dec 2025 15:08:07 +0100 Subject: [PATCH] Enhance distance handling in routing logic: add new distance attributes, improve fallback logic, and refine Azure Maps API integration. --- src/frontend/src/components/UI/Toast.vue | 2 +- .../layout/calculation/TheDashboard.vue | 8 +- src/frontend/src/store/bulkOperation.js | 2 +- src/frontend/src/store/dashboard.js | 19 +- src/frontend/src/store/notification.js | 3 + .../de/avatic/lcc/config/SecurityConfig.java | 4 +- .../CalculationProcessingOverviewDTO.java | 10 +- .../avatic/lcc/model/db/nodes/Distance.java | 9 + .../model/db/premises/route/Destination.java | 10 + .../model/db/premises/route/RouteSection.java | 10 + .../DistanceMatrixRepository.java | 31 ++- .../premise/DestinationRepository.java | 11 +- .../premise/RouteSectionRepository.java | 9 +- .../service/access/DestinationService.java | 27 +- .../lcc/service/api/DistanceApiService.java | 261 ++++++++++++------ .../avatic/lcc/service/api/GeoApiService.java | 1 + .../service/calculation/DistanceService.java | 20 +- .../service/calculation/RoutingService.java | 16 +- ...culationJobProcessorManagementService.java | 3 +- .../RouteSectionCostCalculationService.java | 30 +- .../db/migration/V11__Schedule_Priority.sql | 10 +- src/main/resources/schema.sql | 2 + 22 files changed, 357 insertions(+), 141 deletions(-) diff --git a/src/frontend/src/components/UI/Toast.vue b/src/frontend/src/components/UI/Toast.vue index 27967cc..3c646df 100644 --- a/src/frontend/src/components/UI/Toast.vue +++ b/src/frontend/src/components/UI/Toast.vue @@ -116,7 +116,7 @@ export default { case 'warning': return 'secondary' case 'info': - return 'primary' + return 'secondary' case 'error': return 'exception' default: diff --git a/src/frontend/src/components/layout/calculation/TheDashboard.vue b/src/frontend/src/components/layout/calculation/TheDashboard.vue index 9415703..cf14520 100644 --- a/src/frontend/src/components/layout/calculation/TheDashboard.vue +++ b/src/frontend/src/components/layout/calculation/TheDashboard.vue @@ -13,9 +13,9 @@
-
{{ total }}
+
{{ completed }}
-
Total
+
Completed
@@ -94,8 +94,8 @@ export default { }, computed: { ...mapStores(useDashboardStore), - total() { - return this.dashboardStore.total + completed() { + return this.dashboardStore.completed }, drafts() { return this.dashboardStore.drafts diff --git a/src/frontend/src/store/bulkOperation.js b/src/frontend/src/store/bulkOperation.js index 437a4bc..bfc5e18 100644 --- a/src/frontend/src/store/bulkOperation.js +++ b/src/frontend/src/store/bulkOperation.js @@ -43,7 +43,7 @@ export const useBulkOperationStore = defineStore('bulkOperation', { useNotificationStore().addNotification({ title: 'Bulk operation', message: 'All your bulk operations have been completed.', - type: 'success', + variant: 'info', icon: 'stack', }) diff --git a/src/frontend/src/store/dashboard.js b/src/frontend/src/store/dashboard.js index 0f0d0c8..9faed48 100644 --- a/src/frontend/src/store/dashboard.js +++ b/src/frontend/src/store/dashboard.js @@ -1,6 +1,7 @@ import {defineStore} from 'pinia' import performRequest from "@/backend.js"; import {config} from '@/config' +import {useNotificationStore} from "@/store/notification.js"; export const useDashboardStore = defineStore('dashboard', { state: () => ({ @@ -9,9 +10,9 @@ export const useDashboardStore = defineStore('dashboard', { pullTimer: null, }), getters: { - total(state) { + completed(state) { if (state.stats) - return state.stats.total; + return state.stats.completed; return null; }, @@ -38,13 +39,23 @@ export const useDashboardStore = defineStore('dashboard', { async load() { const url = `${config.backendUrl}/dashboard`; const resp = await performRequest(this, 'GET', url, null); + + if(this.stats?.running && this.stats.running > 0 && resp.data.running === 0) { + useNotificationStore().addNotification({ + title: 'Calculation', + message: 'All your calculations have been completed.', + variant: 'info', + icon: 'calculator', + }) + } + this.stats = resp.data; }, startPulling() { if (this.pullTimer) return - this.pullTimer = setTimeout(() => { - this.pull() + this.pullTimer = setTimeout(async () => { + await this.pull() }, this.pullInterval) }, stopPulling() { diff --git a/src/frontend/src/store/notification.js b/src/frontend/src/store/notification.js index 018691c..b2fe61d 100644 --- a/src/frontend/src/store/notification.js +++ b/src/frontend/src/store/notification.js @@ -33,6 +33,9 @@ export const useNotificationStore = defineStore('notification', { return this.notifications.pop(); }, addNotification(notification) { + + console.log("add notification", notification, this.notifications.length) + this.notifications.push({ icon: notification.icon ?? null, message: notification.message ?? 'Unknown notification', diff --git a/src/main/java/de/avatic/lcc/config/SecurityConfig.java b/src/main/java/de/avatic/lcc/config/SecurityConfig.java index 079f381..0ce4c0d 100644 --- a/src/main/java/de/avatic/lcc/config/SecurityConfig.java +++ b/src/main/java/de/avatic/lcc/config/SecurityConfig.java @@ -42,7 +42,7 @@ import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.CsrfTokenRequestHandler; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.util.StringUtils; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; @@ -111,7 +111,7 @@ public class SecurityConfig { .exceptionHandling(ex -> ex .defaultAuthenticationEntryPointFor( new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), - new AntPathRequestMatcher("/api/**") + PathPatternRequestMatcher.withDefaults().matcher("/api/**") ) ) .csrf(csrf -> csrf diff --git a/src/main/java/de/avatic/lcc/dto/calculation/execution/CalculationProcessingOverviewDTO.java b/src/main/java/de/avatic/lcc/dto/calculation/execution/CalculationProcessingOverviewDTO.java index 9f6f58d..c92e257 100644 --- a/src/main/java/de/avatic/lcc/dto/calculation/execution/CalculationProcessingOverviewDTO.java +++ b/src/main/java/de/avatic/lcc/dto/calculation/execution/CalculationProcessingOverviewDTO.java @@ -2,7 +2,7 @@ package de.avatic.lcc.dto.calculation.execution; public class CalculationProcessingOverviewDTO { - private Integer total; + private Integer completed; private Integer running; @@ -11,12 +11,12 @@ public class CalculationProcessingOverviewDTO { private Integer failed; - public Integer getTotal() { - return total; + public Integer getCompleted() { + return completed; } - public void setTotal(Integer total) { - this.total = total; + public void setCompleted(Integer completed) { + this.completed = completed; } public Integer getRunning() { diff --git a/src/main/java/de/avatic/lcc/model/db/nodes/Distance.java b/src/main/java/de/avatic/lcc/model/db/nodes/Distance.java index 27546d2..d6f2bda 100644 --- a/src/main/java/de/avatic/lcc/model/db/nodes/Distance.java +++ b/src/main/java/de/avatic/lcc/model/db/nodes/Distance.java @@ -40,6 +40,7 @@ public class Distance { private DistanceMatrixState state; + private Integer retries; private Integer fromNodeId; @@ -144,4 +145,12 @@ public class Distance { public void setToUserNodeId(Integer toUserNodeId) { this.toUserNodeId = toUserNodeId; } + + public Integer getRetries() { + return retries; + } + + public void setRetries(Integer retries) { + this.retries = retries; + } } diff --git a/src/main/java/de/avatic/lcc/model/db/premises/route/Destination.java b/src/main/java/de/avatic/lcc/model/db/premises/route/Destination.java index 0aeb820..f9b82dd 100644 --- a/src/main/java/de/avatic/lcc/model/db/premises/route/Destination.java +++ b/src/main/java/de/avatic/lcc/model/db/premises/route/Destination.java @@ -31,6 +31,8 @@ public class Destination { private Integer countryId; + private BigDecimal distanceD2d; + public Integer getLeadTimeD2d() { return leadTimeD2d; } @@ -134,4 +136,12 @@ public class Destination { public void setDisposalCost(BigDecimal disposalCost) { this.disposalCost = disposalCost; } + + public BigDecimal getDistanceD2d() { + return distanceD2d; + } + + public void setDistanceD2d(BigDecimal distanceD2d) { + this.distanceD2d = distanceD2d; + } } diff --git a/src/main/java/de/avatic/lcc/model/db/premises/route/RouteSection.java b/src/main/java/de/avatic/lcc/model/db/premises/route/RouteSection.java index 36610c2..d6219da 100644 --- a/src/main/java/de/avatic/lcc/model/db/premises/route/RouteSection.java +++ b/src/main/java/de/avatic/lcc/model/db/premises/route/RouteSection.java @@ -27,6 +27,8 @@ public class RouteSection { private Integer toRouteNodeId; + private Double distance; + public RateType getRateType() { return rateType; } @@ -114,4 +116,12 @@ public class RouteSection { public void setToRouteNodeId(Integer toRouteNodeId) { this.toRouteNodeId = toRouteNodeId; } + + public Double getDistance() { + return distance; + } + + public void setDistance(Double distance) { + this.distance = distance; + } } diff --git a/src/main/java/de/avatic/lcc/repositories/DistanceMatrixRepository.java b/src/main/java/de/avatic/lcc/repositories/DistanceMatrixRepository.java index a8274fb..c25a2a8 100644 --- a/src/main/java/de/avatic/lcc/repositories/DistanceMatrixRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/DistanceMatrixRepository.java @@ -29,10 +29,10 @@ public class DistanceMatrixRepository { String fromCol = isUsrFrom ? "from_user_node_id" : "from_node_id"; String toCol = isUsrTo ? "to_user_node_id" : "to_node_id"; - String query = "SELECT * FROM distance_matrix WHERE " + fromCol + " = ? AND " + toCol + " = ? AND state = ?"; + String query = "SELECT * FROM distance_matrix WHERE " + fromCol + " = ? AND " + toCol + " = ?"; var distance = jdbcTemplate.query(query, new DistanceMapper(), - src.getId(), dest.getId(), DistanceMatrixState.VALID.name()); + src.getId(), dest.getId()); if (distance.isEmpty()) return Optional.empty(); @@ -40,6 +40,12 @@ public class DistanceMatrixRepository { return Optional.of(distance.getFirst()); } + @Transactional + public void updateRetries(Integer id) { + String query = "UPDATE distance_matrix SET retries = retries + 1 WHERE id = ?"; + jdbcTemplate.update(query, id); + } + @Transactional public void saveDistance(Distance distance) { try { @@ -119,10 +125,20 @@ public class DistanceMatrixRepository { public Distance mapRow(ResultSet rs, int rowNum) throws SQLException { Distance entity = new Distance(); - entity.setFromNodeId(rs.getInt("from_node_id")); - entity.setToNodeId(rs.getInt("to_node_id")); - entity.setFromNodeId(rs.getInt("from_user_node_id")); - entity.setToNodeId(rs.getInt("to_user_node_id")); + entity.setId(rs.getInt("id")); + + var fromNodeId = rs.getInt("from_node_id"); + entity.setFromNodeId(rs.wasNull() ? null : fromNodeId); + + var toNodeId = rs.getInt("to_node_id"); + entity.setToNodeId(rs.wasNull() ? null : toNodeId); + + var fromUserNodeId = rs.getInt("from_user_node_id"); + entity.setFromUserNodeId(rs.wasNull() ? null : fromUserNodeId); + + var toUserNodeId = rs.getInt("to_user_node_id"); + entity.setToUserNodeId(rs.wasNull() ? null : toUserNodeId); + entity.setDistance(rs.getBigDecimal("distance")); entity.setFromGeoLng(rs.getBigDecimal("from_geo_lng")); entity.setFromGeoLat(rs.getBigDecimal("from_geo_lat")); @@ -131,6 +147,9 @@ public class DistanceMatrixRepository { entity.setState(DistanceMatrixState.valueOf(rs.getString("state"))); entity.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime()); + var retries = rs.getInt("retries"); + entity.setRetries(rs.wasNull() ? 0 : retries); + return entity; } } 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 3b47aa3..70dde7f 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/DestinationRepository.java @@ -67,7 +67,7 @@ public class DestinationRepository { } @Transactional - public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime) { + public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime, BigDecimal distanceD2d) { if (id == null) { throw new InvalidArgumentException("ID cannot be null"); } @@ -99,6 +99,9 @@ public class DestinationRepository { setClauses.add("lead_time_d2d = :d2dLeadTime"); parameters.put("d2dLeadTime", setD2d ? d2dLeadTime : null); + setClauses.add("distance_d2d = :distanceD2d"); + parameters.put("distanceD2d", distanceD2d); + if (annualAmount != null) { setClauses.add("annual_amount = :annualAmount"); @@ -268,7 +271,7 @@ public class DestinationRepository { public Integer insert(Destination destination) { KeyHolder keyHolder = new GeneratedKeyHolder(); - String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, country_id, rate_d2d, lead_time_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost, geo_lat, geo_lng) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, country_id, rate_d2d, lead_time_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost, geo_lat, geo_lng, distance_d2d) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; jdbcTemplate.update(connection -> { var ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); @@ -297,6 +300,8 @@ public class DestinationRepository { ps.setBigDecimal(11, destination.getGeoLat()); ps.setBigDecimal(12, destination.getGeoLng()); + ps.setBigDecimal(13, destination.getDistanceD2d()); + return ps; }, keyHolder); @@ -363,6 +368,8 @@ public class DestinationRepository { entity.setGeoLng(rs.getBigDecimal("geo_lng")); entity.setCountryId(rs.getInt("country_id")); + entity.setDistanceD2d(rs.getBigDecimal("distance_d2d")); + return entity; } } 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 f47ac8d..eb8a373 100644 --- a/src/main/java/de/avatic/lcc/repositories/premise/RouteSectionRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/premise/RouteSectionRepository.java @@ -9,6 +9,7 @@ import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import java.math.BigDecimal; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -45,8 +46,8 @@ public class RouteSectionRepository { } 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, rate_type, is_pre_run, is_main_run, is_post_run, is_outdated) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + String sql = "INSERT INTO premise_route_section (premise_route_id, from_route_node_id, to_route_node_id, list_position, transport_type, rate_type, is_pre_run, is_main_run, is_post_run, is_outdated, distance) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { @@ -61,6 +62,7 @@ public class RouteSectionRepository { ps.setBoolean(8, premiseRouteSection.getMainRun()); ps.setBoolean(9, premiseRouteSection.getPostRun()); ps.setBoolean(10, premiseRouteSection.getOutdated()); + ps.setBigDecimal(11, premiseRouteSection.getDistance() == null ? null : BigDecimal.valueOf(premiseRouteSection.getDistance())); return ps; }, keyHolder); @@ -92,6 +94,9 @@ public class RouteSectionRepository { entity.setPostRun(rs.getBoolean("is_post_run")); entity.setOutdated(rs.getBoolean("is_outdated")); + var distance = rs.getBigDecimal("distance"); + entity.setDistance(rs.wasNull() ? null : distance.doubleValue()); + return entity; } } 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 283fd8d..d7b6297 100644 --- a/src/main/java/de/avatic/lcc/service/access/DestinationService.java +++ b/src/main/java/de/avatic/lcc/service/access/DestinationService.java @@ -10,6 +10,7 @@ import de.avatic.lcc.model.db.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.DistanceService; import de.avatic.lcc.service.calculation.RoutingService; import de.avatic.lcc.service.transformer.premise.DestinationTransformer; import de.avatic.lcc.service.users.AuthorizationService; @@ -35,8 +36,9 @@ public class DestinationService { private final PremiseRepository premiseRepository; private final UserNodeRepository userNodeRepository; private final AuthorizationService authorizationService; + private final DistanceService distanceService; - public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, AuthorizationService authorizationService) { + public DestinationService(DestinationRepository destinationRepository, DestinationTransformer destinationTransformer, RouteRepository routeRepository, RouteSectionRepository routeSectionRepository, RouteNodeRepository routeNodeRepository, RoutingService routingService, NodeRepository nodeRepository, PremiseRepository premiseRepository, UserNodeRepository userNodeRepository, AuthorizationService authorizationService, DistanceService distanceService) { this.destinationRepository = destinationRepository; this.destinationTransformer = destinationTransformer; this.routeRepository = routeRepository; @@ -47,6 +49,7 @@ public class DestinationService { this.premiseRepository = premiseRepository; this.userNodeRepository = userNodeRepository; this.authorizationService = authorizationService; + this.distanceService = distanceService; } @@ -199,11 +202,30 @@ public class DestinationService { destinationUpdateDTO.getDisposalCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getDisposalCost().doubleValue()), destinationUpdateDTO.getHandlingCost() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getHandlingCost().doubleValue()), destinationUpdateDTO.getD2d(), destinationUpdateDTO.getRateD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getRateD2d().doubleValue()), - destinationUpdateDTO.getLeadtimeD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getLeadtimeD2d()) + destinationUpdateDTO.getLeadtimeD2d() == null ? null : BigDecimal.valueOf(destinationUpdateDTO.getLeadtimeD2d()), + destinationUpdateDTO.getD2d() ? getD2dDistance(id) : null ); } + private BigDecimal getD2dDistance(Integer destinationId) { + var dest = destinationRepository.getById(destinationId); + + if(dest.isPresent()) { + var premise = premiseRepository.getPremiseById(dest.get().getPremiseId()); + + if(premise.isPresent()) { + boolean isUserNode = premise.get().getSupplierNodeId() == null; + + var from = isUserNode ? userNodeRepository.getById(premise.get().getUserSupplierNodeId()) : nodeRepository.getById(premise.get().getSupplierNodeId()); + var to = nodeRepository.getById(dest.get().getDestinationNodeId()); + + return BigDecimal.valueOf(distanceService.getDistanceForNode(from.orElseThrow(), to.orElseThrow())); + } + } + return null; + } + private Map> findRoutes(List premisses, Map> routingRequest) { Map> routes = new HashMap<>(); @@ -267,6 +289,7 @@ public class DestinationService { premiseRouteSection.setFromRouteNodeId(fromNodeId); premiseRouteSection.setToRouteNodeId(toNodeId); + routeSectionRepository.insert(premiseRouteSection); fromNodeId = toNodeId; diff --git a/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java b/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java index f8b6b38..1150ebd 100644 --- a/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java +++ b/src/main/java/de/avatic/lcc/service/api/DistanceApiService.java @@ -1,5 +1,7 @@ package de.avatic.lcc.service.api; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import de.avatic.lcc.model.azuremaps.route.RouteDirectionsResponse; import de.avatic.lcc.model.db.nodes.Distance; import de.avatic.lcc.model.db.nodes.DistanceMatrixState; @@ -10,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -25,14 +28,18 @@ public class DistanceApiService { private final DistanceMatrixRepository distanceMatrixRepository; private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + private final GeoApiService geoApiService; @Value("${azure.maps.subscription.key}") private String subscriptionKey; public DistanceApiService(DistanceMatrixRepository distanceMatrixRepository, - RestTemplate restTemplate) { + RestTemplate restTemplate, ObjectMapper objectMapper, GeoApiService geoApiService) { this.distanceMatrixRepository = distanceMatrixRepository; this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + this.geoApiService = geoApiService; } public Optional getDistance(Location from, Location to) { @@ -70,22 +77,55 @@ public class DistanceApiService { Optional cachedDistance = distanceMatrixRepository.getDistance(from, isUsrFrom, to, isUsrTo); - if (cachedDistance.isPresent()) { + if (cachedDistance.isPresent() && cachedDistance.get().getState() == DistanceMatrixState.VALID) { logger.info("Found cached distance from node {} to node {}", from.getExternalMappingId(), to.getExternalMappingId()); return cachedDistance; } - logger.info("Fetching distance from Azure Maps for nodes {} to {}", from.getExternalMappingId(), to.getExternalMappingId()); - Optional fetchedDistance = fetchDistanceFromAzureMaps(from, isUsrFrom, to, isUsrTo); + if (cachedDistance.isPresent() && cachedDistance.get().getState() == DistanceMatrixState.EXCEPTION) { + if (cachedDistance.get().getRetries() >= 3) + return Optional.empty(); - if (fetchedDistance.isPresent()) { - distanceMatrixRepository.saveDistance(fetchedDistance.get()); - return fetchedDistance; + distanceMatrixRepository.updateRetries(cachedDistance.get().getId()); + } + + logger.info("Fetching distance from Azure Maps for nodes {} to {}", from.getExternalMappingId(), to.getExternalMappingId()); + AzureMapResponse distanceResponse = fetchDistanceFromAzureMaps(from, isUsrFrom, to, isUsrTo, true); + + if (distanceResponse.distance != null) { + distanceMatrixRepository.saveDistance(distanceResponse.distance); + return Optional.of(distanceResponse.distance); + } + + if(distanceResponse.errorType != AzureMapsErrorType.NO_ERROR) { + distanceMatrixRepository.saveDistance(getErrorDistance(cachedDistance, from, isUsrFrom, to, isUsrTo)); } return Optional.empty(); } + private Distance getErrorDistance(Optional cachedDistance, Node from, boolean isUsrFrom, Node to, boolean isUsrTo) { + var distance = cachedDistance.orElse(new Distance()); + + distance.setState(DistanceMatrixState.EXCEPTION); + distance.setUpdatedAt(LocalDateTime.now()); + distance.setRetries(distance.getRetries() == null ? 0 : distance.getRetries() + 1); + + if(cachedDistance.isEmpty()) { + distance.setFromUserNodeId(isUsrFrom ? from.getId() : null); + distance.setFromNodeId(isUsrFrom ? null : from.getId()); + distance.setToUserNodeId(isUsrTo ? to.getId() : null); + distance.setToNodeId(isUsrTo ? null : to.getId()); + + distance.setFromGeoLat(from.getGeoLat()); + distance.setFromGeoLng(from.getGeoLng()); + distance.setToGeoLat(to.getGeoLat()); + distance.setToGeoLng(to.getGeoLng()); + } + + return distance; + } + private RouteDirectionsResponse fetchDistanceFromAzureMaps(BigDecimal fromLat, BigDecimal fromLng, BigDecimal toLat, BigDecimal toLng) { String url = UriComponentsBuilder.fromUriString(AZURE_MAPS_ROUTE_API) .queryParam("api-version", "1.0") @@ -99,89 +139,144 @@ public class DistanceApiService { return restTemplate.getForObject(url, RouteDirectionsResponse.class); } - //TODO; -// private Optional handleAzureMapsError(HttpClientErrorException e, -// Node from, boolean isUsrFrom, -// Node to, boolean isUsrTo) { -// try { -// String responseBody = e.getResponseBodyAsString(); -// JsonNode errorNode = objectMapper.readTree(responseBody); -// -// String errorCode = errorNode.path("error").path("code").asText(); -// String errorMessage = errorNode.path("error").path("message").asText(); -// -// logger.warn("Azure Maps API Error for nodes {} to {}: {} - {}", -// from.getExternalMappingId(), to.getExternalMappingId(), -// errorCode, errorMessage); -// -// // Spezifische Fehlerbehandlung -// if (errorMessage.contains("MAP_MATCHING_FAILURE") || -// errorMessage.contains("NO_ROUTE_FOUND")) { -// -// logger.info("Route calculation failed, falling back to straight line distance"); -// -// // Fallback auf Luftlinie -// double straightLineKm = calculateHaversineDistance( -// from.getGeoLat(), from.getGeoLng(), -// to.getGeoLat(), to.getGeoLng() -// ); -// -// return createStraightLineDistance(from, isUsrFrom, to, isUsrTo, straightLineKm); -// } -// -// } catch (Exception parseException) { -// logger.error("Failed to parse Azure Maps error response", parseException); -// } -// -// return Optional.empty(); -// } - private Optional fetchDistanceFromAzureMaps(Node from, boolean isUsrFrom, Node to, boolean isUsrTo) { + private AzureMapResponse fetchDistanceFromAzureMaps(Node from, boolean isUsrFrom, Node to, boolean isUsrTo, boolean allowFixing) { try { - RouteDirectionsResponse response = fetchDistanceFromAzureMaps(from.getGeoLat(), from.getGeoLng(), to.getGeoLat(), to.getGeoLng()); - - if (response != null && response.getRoutes() != null && !response.getRoutes().isEmpty()) { - Integer distanceInMeters = response.getRoutes().getFirst().getSummary().getLengthInMeters(); - - Distance distance = new Distance(); - - if (isUsrFrom) { - distance.setFromUserNodeId(from.getId()); - distance.setFromNodeId(null); - } else { - distance.setFromUserNodeId(null); - distance.setFromNodeId(from.getId()); - } - - if (isUsrTo) { - distance.setToUserNodeId(to.getId()); - distance.setToNodeId(null); - } else { - distance.setToUserNodeId(null); - distance.setToNodeId(to.getId()); - } - - distance.setFromGeoLat(from.getGeoLat()); - distance.setFromGeoLng(from.getGeoLng()); - distance.setToGeoLat(to.getGeoLat()); - distance.setToGeoLng(to.getGeoLng()); - distance.setDistance(BigDecimal.valueOf(distanceInMeters)); - distance.setState(DistanceMatrixState.VALID); - distance.setUpdatedAt(LocalDateTime.now()); - - logger.info("Successfully fetched distance: {} meters", distanceInMeters); - return Optional.of(distance); - } else { - logger.warn("No routes found in Azure Maps response"); - } + return convertToDistance(response, from, isUsrFrom, to, isUsrTo); } catch (Exception e) { - //TODO parse 400 Bad Request on GET request for "https://atlas.microsoft.com/route/directions/json": "{ "error": { "code": "400 BadRequest", "message": "Engine error while executing route request: MAP_MATCHING_FAILURE: Destination (31.364, 121.598)" }} - // "{ "error": { "code": "400 BadRequest", "message": "Engine error while executing route request: NO_ROUTE_FOUND: Origin and destination have different ProductId's." }}" + if (HttpClientErrorException.class.isAssignableFrom(e.getClass())) + return handleAzureMapsError((HttpClientErrorException) e, from, isUsrFrom, to, isUsrTo, allowFixing); + logger.error("Error fetching distance from Azure Maps", e); + return new AzureMapResponse(null, AzureMapsErrorType.OTHER_ERROR, null); + } + } + + private AzureMapResponse convertToDistance(RouteDirectionsResponse response, Node from, boolean isUsrFrom, Node to, boolean isUsrTo) { + if (response != null && response.getRoutes() != null && !response.getRoutes().isEmpty()) { + Integer distanceInMeters = response.getRoutes().getFirst().getSummary().getLengthInMeters(); + + Distance distance = new Distance(); + + if (isUsrFrom) { + distance.setFromUserNodeId(from.getId()); + distance.setFromNodeId(null); + } else { + distance.setFromUserNodeId(null); + distance.setFromNodeId(from.getId()); + } + + if (isUsrTo) { + distance.setToUserNodeId(to.getId()); + distance.setToNodeId(null); + } else { + distance.setToUserNodeId(null); + distance.setToNodeId(to.getId()); + } + + distance.setFromGeoLat(from.getGeoLat()); + distance.setFromGeoLng(from.getGeoLng()); + distance.setToGeoLat(to.getGeoLat()); + distance.setToGeoLng(to.getGeoLng()); + distance.setDistance(BigDecimal.valueOf(distanceInMeters)); + distance.setState(DistanceMatrixState.VALID); + distance.setUpdatedAt(LocalDateTime.now()); + + // reset to 0 if on success + distance.setRetries(0); + + logger.info("Successfully fetched distance: {} meters", distanceInMeters); + return new AzureMapResponse(distance, AzureMapsErrorType.NO_ERROR, null); + } else { + logger.error("No routes found in Azure Maps response"); + return new AzureMapResponse(null, AzureMapsErrorType.ROUTE_ERROR, null); + } + } + + private AzureMapResponse handleAzureMapsError(HttpClientErrorException e, + Node from, boolean isUsrFrom, + Node to, boolean isUsrTo, boolean allowFixing) { + + try { + String responseBody = e.getResponseBodyAsString(); + JsonNode errorNode = objectMapper.readTree(responseBody); + + String errorCode = errorNode.path("error").path("code").asText(); + String errorMessage = errorNode.path("error").path("message").asText(); + + logger.warn("Azure Maps API Error for nodes {} ({}) to {} ({}): {} - {}", + from.getExternalMappingId(), from.getId(), to.getExternalMappingId(), to.getId(), + errorCode, errorMessage); + + if (errorMessage.contains("NO_ROUTE_FOUND")) + return new AzureMapResponse(null, AzureMapsErrorType.ROUTE_ERROR, null); + + if (errorMessage.contains("MAP_MATCHING_FAILURE")) { + if (errorMessage.contains("Destination")) { + if (allowFixing) { + var fixedNode = fixNode(to); + + if (fixedNode != null) + return fetchDistanceFromAzureMaps(from, isUsrFrom, fixedNode, isUsrTo, false); + } + + return new AzureMapResponse(null, AzureMapsErrorType.NODE_ERROR, AzureMapsDefectiveNode.FROM); + } + + if (errorMessage.contains("Origin")) { + if (allowFixing) { + var fixedNode = fixNode(from); + + if (fixedNode != null) + return fetchDistanceFromAzureMaps(fixedNode, isUsrFrom, to, isUsrTo, false); + } + + return new AzureMapResponse(null, AzureMapsErrorType.NODE_ERROR, AzureMapsDefectiveNode.TO); + } + + return new AzureMapResponse(null, AzureMapsErrorType.NODE_ERROR, AzureMapsDefectiveNode.UNKNOWN); + } + + } catch (Exception parseException) { + logger.error("Failed to parse Azure Maps error response", parseException); } - return Optional.empty(); + return new AzureMapResponse(null, AzureMapsErrorType.OTHER_ERROR, null); + } + + private Node fixNode(Node node) { + + logger.info("Try to fix node {} ({}) ", node.getExternalMappingId(), node.getId()); + + Location location = geoApiService.locate(node.getAddress() + ", " + node.getCountryId()); + + if (location != null && location.getLatitude() != null && location.getLongitude() != null) { + if (0 != BigDecimal.valueOf(location.getLatitude()).compareTo(node.getGeoLat()) || 0 != BigDecimal.valueOf(location.getLongitude()).compareTo(node.getGeoLng())) { + logger.info("Fixed node {} ({}) coordinates {}, {} -> {}, {}", node.getExternalMappingId(), node.getId(), node.getGeoLat(), node.getGeoLng(), location.getLatitude(), location.getLongitude()); + + node.setGeoLng(BigDecimal.valueOf(location.getLongitude())); + node.setGeoLat(BigDecimal.valueOf(location.getLatitude())); + + + return node; + } + } + + return null; + } + + private enum AzureMapsErrorType { + NO_ERROR, NODE_ERROR, ROUTE_ERROR, OTHER_ERROR + } + + private enum AzureMapsDefectiveNode { + FROM, TO, UNKNOWN + } + + private record AzureMapResponse(Distance distance, AzureMapsErrorType errorType, + AzureMapsDefectiveNode defectiveNode) { + } diff --git a/src/main/java/de/avatic/lcc/service/api/GeoApiService.java b/src/main/java/de/avatic/lcc/service/api/GeoApiService.java index 1bc63f2..0bae27b 100644 --- a/src/main/java/de/avatic/lcc/service/api/GeoApiService.java +++ b/src/main/java/de/avatic/lcc/service/api/GeoApiService.java @@ -43,6 +43,7 @@ public class GeoApiService { } public GeocodingResult geocode(String address) { + if (address == null || address.trim().isEmpty()) { logger.warn("Address is null or empty"); return null; diff --git a/src/main/java/de/avatic/lcc/service/calculation/DistanceService.java b/src/main/java/de/avatic/lcc/service/calculation/DistanceService.java index a606ec1..a09a3e7 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/DistanceService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/DistanceService.java @@ -4,7 +4,6 @@ import de.avatic.lcc.model.db.country.DetourIndex; import de.avatic.lcc.model.db.country.IsoCode; import de.avatic.lcc.model.db.nodes.Location; import de.avatic.lcc.model.db.nodes.Node; -import de.avatic.lcc.repositories.DistanceMatrixRepository; import de.avatic.lcc.repositories.country.CountryRepository; import de.avatic.lcc.service.api.DistanceApiService; import org.springframework.stereotype.Service; @@ -21,29 +20,36 @@ public class DistanceService { this.distanceApiService = distanceApiService; } - public double getDistance(Integer srcCountryId, Location src, Integer destCountryId, Location dest, boolean fast) { - if (fast) return getDistanceFast(srcCountryId, src, destCountryId, dest); + public double getDistanceForLocation(Integer srcCountryId, Location src, Integer destCountryId, Location dest, boolean fast) { + var fastDistance = getDistanceFast(srcCountryId, src, destCountryId, dest); + + // use fast distance if more than 3000km + if (fast || fastDistance > 3000) return fastDistance; var distance = distanceApiService.getDistance(src, dest); - if (distance.isEmpty()) return getDistanceFast(srcCountryId, src, destCountryId, dest); + if (distance.isEmpty()) return fastDistance; return distance.map(value -> value/1000).orElse(0); } - public double getDistance(Node src, Node dest, boolean fast) { + public double getDistanceForNode(Node src, Node dest) { + return getDistanceForNode(src, dest, false); + } + + public double getDistanceForNode(Node src, Node dest, boolean fast) { var fastDistance = getDistanceFast(src, dest); // use fast distance if more than 3000km if (fast || fastDistance > 3000) return fastDistance; var distance = distanceApiService.getDistance(src, src.isUserNode(), dest, dest.isUserNode()); - if (distance.isEmpty()) return getDistanceFast(src, dest); + if (distance.isEmpty()) return fastDistance; return distance.map(value -> value.getDistance().intValue()/1000).orElse(0); } - public double getDistanceFast(Integer srcCountryId, Location src, Integer destCountryId, Location dest ) { + private double getDistanceFast(Integer srcCountryId, Location src, Integer destCountryId, Location dest ) { double srcLatitudeRadians = Math.toRadians(src.getLatitude()); double srcLongitudeRad = Math.toRadians(src.getLongitude()); double destLatitudeRadians = Math.toRadians(dest.getLatitude()); 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 5f474f9..a23d92c 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/RoutingService.java @@ -219,6 +219,8 @@ public class RoutingService { routeSection.setPostRun(section.getType().equals(TemporaryRateObject.TemporaryRateObjectType.POST_RUN)); routeSection.setPreRun(false); + routeSection.setDistance(section.getApproxDistance()); + return routeSection; } @@ -334,7 +336,7 @@ public class RoutingService { } finalSection.setRate(matrixRate); - finalSection.setApproxDistance(distanceService.getDistance(container.getSourceNode(), toNode, true)); + finalSection.setApproxDistance(distanceService.getDistanceForNode(container.getSourceNode(), toNode)); rates.add(finalSection); } @@ -699,7 +701,7 @@ public class RoutingService { if (matrixRate.isPresent()) { matrixRateObj.setRate(matrixRate.get()); - matrixRateObj.setApproxDistance(distanceService.getDistance(startNode, endNode, true)); + matrixRateObj.setApproxDistance(distanceService.getDistanceForNode(startNode, endNode)); container.getRates().add(matrixRateObj); return matrixRateObj; } else { @@ -927,16 +929,16 @@ public class RoutingService { sb.append(sections.getLast().getFromNode().getDebugText()); for (var section : sections.reversed()) { - sb.append(" -"); + sb.append(" --["); sb.append(section.getType().getDebugText()); - sb.append("-> "); + sb.append("]--> "); sb.append(section.getToNode().getDebugText()); } } else sb.append("Empty"); - return String.format("Route: [%s, %s]", quality.name().charAt(0) + (isFastest ? "\uD83D\uDE80" : "") + (isCheapest ? "\uD83D\uDCB2" : ""), sb); + return String.format("Route: [%s, %s]", quality.name() + (isFastest ? " FAST" : "") + (isCheapest ? " CHEAP" : ""), sb); } public void setQuality(ChainQuality quality) { @@ -1074,8 +1076,8 @@ public class RoutingService { } private enum TemporaryRateObjectType { - NEAR_BY("\uD83D\uDCCD"), MATRIX("\uD83D\uDCCA"), CONTAINER("\uD83D\uDCE6"), POST_RUN("\uD83D\uDE9B"), MAIN_RUN("\uD83D\uDEA2"); - +// NEAR_BY("\uD83D\uDCCD"), MATRIX("\uD83D\uDCCA"), CONTAINER("\uD83D\uDCE6"), POST_RUN("\uD83D\uDE9B"), MAIN_RUN("\uD83D\uDEA2"); + NEAR_BY("NEAR"), MATRIX("KM"), CONTAINER("CON"), POST_RUN("POST"), MAIN_RUN("MAIN"); private final String debugText; TemporaryRateObjectType(String debugText) { diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationJobProcessorManagementService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationJobProcessorManagementService.java index 2cf6d26..0fb6949 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationJobProcessorManagementService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/CalculationJobProcessorManagementService.java @@ -2,7 +2,6 @@ package de.avatic.lcc.service.calculation.execution; import de.avatic.lcc.dto.calculation.execution.*; import de.avatic.lcc.model.db.calculations.CalculationJob; -import de.avatic.lcc.model.db.calculations.CalculationJobPriority; import de.avatic.lcc.model.db.calculations.CalculationJobState; import de.avatic.lcc.model.db.premises.PremiseState; import de.avatic.lcc.model.db.properties.PropertySet; @@ -126,7 +125,7 @@ public class CalculationJobProcessorManagementService { dto.setFailed(failed); dto.setDrafts(draft); dto.setRunning(running); - dto.setTotal(completed + draft); + dto.setCompleted(completed); return dto; } diff --git a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java index ae22e04..377c058 100644 --- a/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java +++ b/src/main/java/de/avatic/lcc/service/calculation/execution/steps/RouteSectionCostCalculationService.java @@ -1,9 +1,9 @@ package de.avatic.lcc.service.calculation.execution.steps; -import de.avatic.lcc.model.calculation.ContainerCalculationResult; import de.avatic.lcc.dto.generic.ContainerType; import de.avatic.lcc.dto.generic.RateType; import de.avatic.lcc.dto.generic.TransportType; +import de.avatic.lcc.model.calculation.ContainerCalculationResult; import de.avatic.lcc.model.db.calculations.CalculationJobRouteSection; import de.avatic.lcc.model.db.nodes.Location; import de.avatic.lcc.model.db.nodes.Node; @@ -70,7 +70,7 @@ public class RouteSectionCostCalculationService { Node fromNode = premise.getSupplierNodeId() != null ? nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow() : userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow(); Node toNode = nodeRepository.getById(destination.getDestinationNodeId()).orElseThrow(); - double distance = distanceService.getDistance(fromNode, toNode, false); + double distance = destination.getDistanceD2d() == null ? distanceService.getDistanceForNode(fromNode, toNode) : destination.getDistanceD2d().doubleValue(); result.setDistance(BigDecimal.valueOf(distance)); // Get rate and transit time @@ -130,8 +130,14 @@ public class RouteSectionCostCalculationService { // Get nodes and distance RouteNode fromNode = routeNodeRepository.getById(section.getFromRouteNodeId()).orElseThrow(); RouteNode toNode = routeNodeRepository.getById(section.getToRouteNodeId()).orElseThrow(); - double distance = getDistance(fromNode, toNode); - result.setDistance(BigDecimal.valueOf(distance)); + + if (section.getDistance() == null) { + double distance = getDistance(fromNode, toNode); + result.setDistance(BigDecimal.valueOf(distance)); + } else { + result.setDistance(BigDecimal.valueOf(section.getDistance())); + } + // Get rate and transit time BigDecimal rate; @@ -143,7 +149,7 @@ public class RouteSectionCostCalculationService { transitTime = containerRate.getLeadTime(); } else if (RateType.MATRIX == section.getRateType()) { MatrixRate matrixRate = findMatrixRate(fromNode, toNode, periodId); - rate = matrixRate.getRate().multiply(BigDecimal.valueOf(distance)); + rate = matrixRate.getRate().multiply(result.getDistance()); transitTime = 3; // Default transit time for matrix rate } else if (RateType.NEAR_BY == section.getRateType()) { rate = BigDecimal.ZERO; @@ -183,8 +189,8 @@ public class RouteSectionCostCalculationService { BigDecimal annualCost = (containerCalculation.isWeightExceeded() ? prices.weightPrice.multiply(annualWeight) : prices.volumePrice.multiply(annualVolume)); - BigDecimal annualRiskCost = useRiskChange ? annualCost.multiply(chanceRiskFactors.getRiskFactor()) : annualCost; - BigDecimal annualChanceCost = useRiskChange ? annualCost.multiply(chanceRiskFactors.getChanceFactor()): annualCost; + BigDecimal annualRiskCost = useRiskChange ? annualCost.multiply(chanceRiskFactors.getRiskFactor()) : annualCost; + BigDecimal annualChanceCost = useRiskChange ? annualCost.multiply(chanceRiskFactors.getChanceFactor()) : annualCost; result.setAnnualRiskCost(annualRiskCost); result.setAnnualChanceCost(annualChanceCost); @@ -278,8 +284,8 @@ public class RouteSectionCostCalculationService { private double getDistance(RouteNode fromNode, RouteNode toNode) { - if(fromNode.getOutdated() || toNode.getOutdated()) { - return distanceService.getDistance( + if (fromNode.getOutdated() || toNode.getOutdated()) { + return distanceService.getDistanceForLocation( fromNode.getCountryId(), new Location(fromNode.getGeoLng().doubleValue(), fromNode.getGeoLat().doubleValue()), toNode.getCountryId(), new Location(toNode.getGeoLng().doubleValue(), toNode.getGeoLat().doubleValue()), false); } @@ -287,14 +293,14 @@ public class RouteSectionCostCalculationService { var optSrcNode = fromNode.getNodeId() == null ? userNodeRepository.getById(fromNode.getUserNodeId()) : nodeRepository.getById(fromNode.getNodeId()); var optDestNode = toNode.getNodeId() == null ? userNodeRepository.getById(toNode.getUserNodeId()) : nodeRepository.getById(toNode.getNodeId()); - if(optSrcNode.isEmpty() ) { + if (optSrcNode.isEmpty()) { throw new NoSuchElementException("Source node not found for route section " + fromNode.getName()); } - if(optDestNode.isEmpty() ) { + if (optDestNode.isEmpty()) { throw new NoSuchElementException("Destination node not found for route section" + toNode.getName()); } - return distanceService.getDistance(optSrcNode.get(), optDestNode.get(), false); + return distanceService.getDistanceForNode(optSrcNode.get(), optDestNode.get(), false); } diff --git a/src/main/resources/db/migration/V11__Schedule_Priority.sql b/src/main/resources/db/migration/V11__Schedule_Priority.sql index f404a71..45fee6f 100644 --- a/src/main/resources/db/migration/V11__Schedule_Priority.sql +++ b/src/main/resources/db/migration/V11__Schedule_Priority.sql @@ -7,5 +7,13 @@ ALTER TABLE calculation_job ADD INDEX idx_priority (priority); ALTER TABLE distance_matrix + ADD COLUMN retries INT NOT NULL DEFAULT 0, DROP CONSTRAINT chk_distance_matrix_state, - ADD CONSTRAINT chk_distance_matrix_state CHECK (`state` IN ('VALID', 'STALE', 'EXCEPTION')); \ No newline at end of file + ADD CONSTRAINT chk_distance_matrix_state CHECK (`state` IN ('VALID', 'STALE', 'EXCEPTION')); + + +ALTER TABLE premise_destination + ADD COLUMN distance_d2d DECIMAL(15, 2) DEFAULT NULL COMMENT 'travel distance between the two nodes in meters'; + +ALTER TABLE premise_route_section + ADD COLUMN distance DECIMAL(15, 2) DEFAULT NULL COMMENT 'travel distance between the two nodes in meters'; \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 24600ca..47c35f3 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -433,6 +433,7 @@ CREATE TABLE IF NOT EXISTS premise_destination is_d2d BOOLEAN DEFAULT FALSE, rate_d2d DECIMAL(15, 2) DEFAULT NULL CHECK (rate_d2d >= 0), lead_time_d2d INT UNSIGNED DEFAULT NULL CHECK (lead_time_d2d >= 0), + distance_d2d DECIMAL(15, 2) DEFAULT NULL, repacking_cost DECIMAL(15, 2) DEFAULT NULL CHECK (repacking_cost >= 0), handling_cost DECIMAL(15, 2) DEFAULT NULL CHECK (handling_cost >= 0), disposal_cost DECIMAL(15, 2) DEFAULT NULL CHECK (disposal_cost >= 0), @@ -496,6 +497,7 @@ CREATE TABLE IF NOT EXISTS premise_route_section is_main_run BOOLEAN DEFAULT FALSE, is_post_run BOOLEAN DEFAULT FALSE, is_outdated BOOLEAN DEFAULT FALSE, + distance DECIMAL(15, 2) DEFAULT NULL COMMENT 'travel distance between the two nodes in meters', CONSTRAINT fk_premise_route_section_premise_route_id FOREIGN KEY (premise_route_id) REFERENCES premise_route (id), FOREIGN KEY (from_route_node_id) REFERENCES premise_route_node (id), FOREIGN KEY (to_route_node_id) REFERENCES premise_route_node (id),