Commit 815273bb20af37aa0e8c8bc10deed38d29a23254

Authored by shaofan
1 parent f3ec2e0c

新增高德电动车路径规划支持,完善配送费计算与路线规划接口

src/main/java/com/diligrp/rider/config/AmapProperties.java 0 → 100644
  1 +package com.diligrp.rider.config;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +import org.springframework.context.annotation.Configuration;
  6 +
  7 +/**
  8 + * 高德 Web 服务 API 配置(电动车路径规划)。
  9 + */
  10 +@Data
  11 +@Configuration
  12 +@ConfigurationProperties(prefix = "amap")
  13 +public class AmapProperties {
  14 +
  15 + /** 是否启用高德 ETA;false 时调用直接返回空,由上游降级到直线距离公式 */
  16 + private boolean enabled = true;
  17 +
  18 + /** 高德 Web 服务 API Key */
  19 + private String key;
  20 +
  21 + /** 高德 Web 服务基础 URL */
  22 + private String baseUrl = "https://restapi.amap.com";
  23 +
  24 + /** HTTP 连接超时(毫秒) */
  25 + private int connectTimeoutMillis = 3000;
  26 +
  27 + /** HTTP 读取超时(毫秒) */
  28 + private int readTimeoutMillis = 3000;
  29 +}
... ...
src/main/java/com/diligrp/rider/controller/OpenApiController.java
... ... @@ -4,15 +4,21 @@ import com.diligrp.rider.common.exception.BizException;
4 4 import com.diligrp.rider.common.result.Result;
5 5 import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
6 6 import com.diligrp.rider.dto.OpenDeliveryFeeCalcDTO;
  7 +import com.diligrp.rider.dto.OpenRouteCalcDTO;
7 8 import com.diligrp.rider.entity.OpenApp;
8 9 import com.diligrp.rider.service.DeliveryFeeService;
9 10 import com.diligrp.rider.service.OpenAppService;
  11 +import com.diligrp.rider.service.amap.AmapRouteClient;
  12 +import com.diligrp.rider.util.GeoUtil;
10 13 import com.diligrp.rider.vo.DeliveryFeeResultVO;
  14 +import com.diligrp.rider.vo.RouteCalcVO;
11 15 import jakarta.servlet.http.HttpServletRequest;
12 16 import jakarta.validation.Valid;
13 17 import lombok.RequiredArgsConstructor;
14 18 import org.springframework.web.bind.annotation.*;
15 19  
  20 +import java.util.Optional;
  21 +
16 22 /**
17 23 * 开放平台对外接口
18 24 * 路径前缀 /api/open/** 受 OpenApiInterceptor 签名鉴权保护
... ... @@ -25,6 +31,7 @@ public class OpenApiController {
25 31  
26 32 private final DeliveryFeeService deliveryFeeService;
27 33 private final OpenAppService openAppService;
  34 + private final AmapRouteClient amapRouteClient;
28 35  
29 36 /**
30 37 * 计算配送费(对外开放版)
... ... @@ -55,6 +62,32 @@ public class OpenApiController {
55 62 return Result.success(deliveryFeeService.isServiceEnabled(resolveCityId(request), orderType));
56 63 }
57 64  
  65 + /**
  66 + * 路线计算:传入起止经纬度,返回距离(米)与导航耗时(秒)。
  67 + * - distance 始终有值:优先高德电动车路径距离,失败时回退到直线距离
  68 + * - duration 高德未命中时为 null
  69 + */
  70 + @PostMapping("/route/calc")
  71 + public Result<RouteCalcVO> routeCalc(@Valid @RequestBody OpenRouteCalcDTO dto) {
  72 + Optional<AmapRouteClient.AmapRoute> route = amapRouteClient.getElectrobikeRoute(
  73 + dto.getStartLng(), dto.getStartLat(), dto.getEndLng(), dto.getEndLat());
  74 + if (route.isPresent()) {
  75 + return Result.success(new RouteCalcVO(route.get().distanceMeters(), route.get().durationSeconds()));
  76 + }
  77 + double km;
  78 + try {
  79 + km = GeoUtil.calcDistanceKm(
  80 + Double.parseDouble(dto.getStartLat()),
  81 + Double.parseDouble(dto.getStartLng()),
  82 + Double.parseDouble(dto.getEndLat()),
  83 + Double.parseDouble(dto.getEndLng()));
  84 + } catch (Exception e) {
  85 + throw new BizException("起止经纬度格式错误");
  86 + }
  87 + int distanceMeters = (int) Math.round(km * 1000);
  88 + return Result.success(new RouteCalcVO(distanceMeters, null));
  89 + }
  90 +
58 91 private Long resolveCityId(HttpServletRequest request) {
59 92 String appKey = request.getHeader("X-App-Key");
60 93 OpenApp app = openAppService.getByAppKey(appKey);
... ...
src/main/java/com/diligrp/rider/dto/OpenRouteCalcDTO.java 0 → 100644
  1 +package com.diligrp.rider.dto;
  2 +
  3 +import jakarta.validation.constraints.NotNull;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 开放平台路线计算请求 DTO。
  8 + */
  9 +@Data
  10 +public class OpenRouteCalcDTO {
  11 +
  12 + /** 起点经度 */
  13 + @NotNull(message = "起点经度不能为空")
  14 + private String startLng;
  15 +
  16 + /** 起点纬度 */
  17 + @NotNull(message = "起点纬度不能为空")
  18 + private String startLat;
  19 +
  20 + /** 终点经度 */
  21 + @NotNull(message = "终点经度不能为空")
  22 + private String endLng;
  23 +
  24 + /** 终点纬度 */
  25 + @NotNull(message = "终点纬度不能为空")
  26 + private String endLat;
  27 +}
... ...
src/main/java/com/diligrp/rider/service/amap/AmapRouteClient.java 0 → 100644
  1 +package com.diligrp.rider.service.amap;
  2 +
  3 +import com.diligrp.rider.config.AmapProperties;
  4 +import com.fasterxml.jackson.databind.JsonNode;
  5 +import com.fasterxml.jackson.databind.ObjectMapper;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +import java.net.URI;
  10 +import java.net.URLEncoder;
  11 +import java.net.http.HttpClient;
  12 +import java.net.http.HttpRequest;
  13 +import java.net.http.HttpResponse;
  14 +import java.nio.charset.StandardCharsets;
  15 +import java.time.Duration;
  16 +import java.util.Optional;
  17 +
  18 +/**
  19 + * 高德电动车路径规划客户端(/v5/direction/electrobike)。
  20 + *
  21 + * 任何失败场景(未启用 / Key 缺失 / 坐标非法 / 起止同点 / HTTP 异常 / status != "1" / paths 空 / 字段缺失)
  22 + * 均返回 Optional.empty(),由调用方决定降级策略。
  23 + */
  24 +@Slf4j
  25 +@Component
  26 +public class AmapRouteClient {
  27 +
  28 + private final AmapProperties amapProperties;
  29 + private final ObjectMapper objectMapper;
  30 + private final HttpClient httpClient;
  31 +
  32 + public AmapRouteClient(AmapProperties amapProperties, ObjectMapper objectMapper) {
  33 + this.amapProperties = amapProperties;
  34 + this.objectMapper = objectMapper;
  35 + this.httpClient = HttpClient.newBuilder()
  36 + .connectTimeout(Duration.ofMillis(amapProperties.getConnectTimeoutMillis()))
  37 + .build();
  38 + }
  39 +
  40 + /** 高德电动车路径规划结果:原始距离(米)与耗时(秒)。 */
  41 + public record AmapRoute(int distanceMeters, int durationSeconds) {}
  42 +
  43 + /**
  44 + * 调用高德电动车路径规划,返回原始距离(米)+ 耗时(秒)。
  45 + */
  46 + public Optional<AmapRoute> getElectrobikeRoute(String startLng, String startLat,
  47 + String endLng, String endLat) {
  48 + if (!amapProperties.isEnabled()) {
  49 + return Optional.empty();
  50 + }
  51 + if (amapProperties.getKey() == null || amapProperties.getKey().isBlank()) {
  52 + log.warn("Amap key 未配置,跳过高德调用");
  53 + return Optional.empty();
  54 + }
  55 +
  56 + double sLng;
  57 + double sLat;
  58 + double eLng;
  59 + double eLat;
  60 + try {
  61 + sLng = Double.parseDouble(startLng);
  62 + sLat = Double.parseDouble(startLat);
  63 + eLng = Double.parseDouble(endLng);
  64 + eLat = Double.parseDouble(endLat);
  65 + } catch (Exception e) {
  66 + return Optional.empty();
  67 + }
  68 + if (sLng == eLng && sLat == eLat) {
  69 + return Optional.empty();
  70 + }
  71 +
  72 + String origin = String.format("%.6f,%.6f", sLng, sLat);
  73 + String destination = String.format("%.6f,%.6f", eLng, eLat);
  74 + String url = amapProperties.getBaseUrl() + "/v5/direction/electrobike"
  75 + + "?key=" + URLEncoder.encode(amapProperties.getKey(), StandardCharsets.UTF_8)
  76 + + "&origin=" + URLEncoder.encode(origin, StandardCharsets.UTF_8)
  77 + + "&destination=" + URLEncoder.encode(destination, StandardCharsets.UTF_8)
  78 + + "&show_fields=cost";
  79 +
  80 + try {
  81 + HttpRequest request = HttpRequest.newBuilder()
  82 + .uri(URI.create(url))
  83 + .timeout(Duration.ofMillis(amapProperties.getReadTimeoutMillis()))
  84 + .GET()
  85 + .build();
  86 + HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
  87 + if (response.statusCode() != 200) {
  88 + log.warn("Amap electrobike HTTP {} body={}", response.statusCode(), trim(response.body()));
  89 + return Optional.empty();
  90 + }
  91 + JsonNode root = objectMapper.readTree(response.body());
  92 + String status = root.path("status").asText();
  93 + if (!"1".equals(status)) {
  94 + log.warn("Amap electrobike status={} info={} infocode={}",
  95 + status, root.path("info").asText(), root.path("infocode").asText());
  96 + return Optional.empty();
  97 + }
  98 + JsonNode paths = root.path("route").path("paths");
  99 + if (!paths.isArray() || paths.isEmpty()) {
  100 + log.warn("Amap electrobike paths 空 body={}", trim(response.body()));
  101 + return Optional.empty();
  102 + }
  103 + JsonNode first = paths.get(0);
  104 + long distanceMeters = first.path("distance").asLong(-1);
  105 + long durationSeconds = first.path("cost").path("duration").asLong(-1);
  106 + if (distanceMeters < 0 || durationSeconds <= 0) {
  107 + log.warn("Amap electrobike 字段异常 distance={} duration={} body={}",
  108 + distanceMeters, durationSeconds, trim(response.body()));
  109 + return Optional.empty();
  110 + }
  111 + return Optional.of(new AmapRoute((int) distanceMeters, (int) durationSeconds));
  112 + } catch (Exception e) {
  113 + log.warn("Amap electrobike 调用失败 err={}", e.getMessage());
  114 + return Optional.empty();
  115 + }
  116 + }
  117 +
  118 + /**
  119 + * 返回预计骑行分钟数(向上取整)。委托 {@link #getElectrobikeRoute}。
  120 + */
  121 + public Optional<Integer> getElectrobikeDurationMinutes(String startLng, String startLat,
  122 + String endLng, String endLat) {
  123 + return getElectrobikeRoute(startLng, startLat, endLng, endLat)
  124 + .map(r -> (int) Math.ceil(r.durationSeconds() / 60.0));
  125 + }
  126 +
  127 + private String trim(String body) {
  128 + if (body == null) return "";
  129 + return body.length() > 300 ? body.substring(0, 300) : body;
  130 + }
  131 +}
... ...
src/main/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImpl.java
... ... @@ -6,6 +6,7 @@ import com.diligrp.rider.dto.DeliveryPricingRuleDTO;
6 6 import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
7 7 import com.diligrp.rider.service.CityService;
8 8 import com.diligrp.rider.service.DeliveryFeeService;
  9 +import com.diligrp.rider.service.amap.AmapRouteClient;
9 10 import com.diligrp.rider.util.GeoUtil;
10 11 import com.diligrp.rider.vo.DeliveryFeeResultVO;
11 12 import lombok.RequiredArgsConstructor;
... ... @@ -20,6 +21,7 @@ import java.time.ZonedDateTime;
20 21 import java.util.ArrayList;
21 22 import java.util.Comparator;
22 23 import java.util.List;
  24 +import java.util.Optional;
23 25  
24 26 /**
25 27 * 配送费计算引擎
... ... @@ -31,6 +33,7 @@ import java.util.List;
31 33 public class DeliveryFeeServiceImpl implements DeliveryFeeService {
32 34  
33 35 private final CityService cityService;
  36 + private final AmapRouteClient amapRouteClient;
34 37  
35 38 @Override
36 39 public DeliveryFeeResultVO calcFee(DeliveryFeeCalcDTO dto) {
... ... @@ -182,7 +185,14 @@ public class DeliveryFeeServiceImpl implements DeliveryFeeService {
182 185  
183 186 BigDecimal total = moneyBasic.add(moneyDistance).add(moneyWeight).add(moneyPiece).add(moneyTime);
184 187 result.setTotalFee(applyMinFee(total, typeConfig, result));
185   - result.setEstimatedMinutes(calcEstimatedMinutes(pricingConfig, distanceKm));
  188 +
  189 + // ETA:优先取高德电动车路径耗时,失败降级到 distance_basic_time + 公式
  190 + Optional<Integer> amapMinutes = amapRouteClient.getElectrobikeDurationMinutes(
  191 + dto.getStartLng(), dto.getStartLat(), dto.getEndLng(), dto.getEndLat());
  192 + int estimatedMinutes = amapMinutes.isPresent()
  193 + ? amapMinutes.get()
  194 + : calcEstimatedMinutes(pricingConfig, distanceKm);
  195 + result.setEstimatedMinutes(estimatedMinutes);
186 196  
187 197 return result;
188 198 }
... ...
src/main/java/com/diligrp/rider/vo/RouteCalcVO.java 0 → 100644
  1 +package com.diligrp.rider.vo;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Data;
  5 +import lombok.NoArgsConstructor;
  6 +
  7 +/**
  8 + * 开放平台路线计算结果 VO。
  9 + */
  10 +@Data
  11 +@NoArgsConstructor
  12 +@AllArgsConstructor
  13 +public class RouteCalcVO {
  14 +
  15 + /** 距离(米)。高德命中时为路径距离,否则为直线距离向上取整。 */
  16 + private Integer distance;
  17 +
  18 + /** 导航耗时(秒)。高德未命中时为 null。 */
  19 + private Integer duration;
  20 +}
... ...
src/main/resources/application.yml
... ... @@ -32,12 +32,19 @@ jwt:
32 32 expire: 604800 # 7天,单位秒
33 33  
34 34 jpush:
35   - enabled: false # 未配置 AppKey 前置 false,避免启动时报错
  35 + enabled: true # 未配置 AppKey 前置 false,避免启动时报错
36 36 app-key: fd6e826ae6e67eaf7a7062c4
37 37 master-secret: 6eaa9a8d11493b56812b6ee9
38 38 apns-production: false # iOS APNs:true=生产 false=开发
39 39 time-to-live: 86400 # 离线消息保留秒数(默认 1 天)
40 40  
  41 +amap:
  42 + enabled: true # false 时跳过高德调用,ETA 走直线距离 + 公式
  43 + key: 5aa7f0260a51dcb2f1dda3130f9a0dbd # 高德 Web 服务 API Key,可由环境变量注入
  44 + base-url: https://restapi.amap.com
  45 + connect-timeout-millis: 3000
  46 + read-timeout-millis: 3000
  47 +
41 48 logging:
42 49 level:
43 50 com.diligrp.rider: debug
... ...
src/test/java/com/diligrp/rider/controller/OpenApiControllerTest.java 0 → 100644
  1 +package com.diligrp.rider.controller;
  2 +
  3 +import com.diligrp.rider.common.exception.BizException;
  4 +import com.diligrp.rider.common.result.Result;
  5 +import com.diligrp.rider.dto.OpenRouteCalcDTO;
  6 +import com.diligrp.rider.service.DeliveryFeeService;
  7 +import com.diligrp.rider.service.OpenAppService;
  8 +import com.diligrp.rider.service.amap.AmapRouteClient;
  9 +import com.diligrp.rider.util.GeoUtil;
  10 +import com.diligrp.rider.vo.RouteCalcVO;
  11 +import org.junit.jupiter.api.Test;
  12 +
  13 +import java.util.Optional;
  14 +
  15 +import static org.junit.jupiter.api.Assertions.assertEquals;
  16 +import static org.junit.jupiter.api.Assertions.assertNotNull;
  17 +import static org.junit.jupiter.api.Assertions.assertNull;
  18 +import static org.junit.jupiter.api.Assertions.assertThrows;
  19 +import static org.mockito.ArgumentMatchers.any;
  20 +import static org.mockito.Mockito.mock;
  21 +import static org.mockito.Mockito.when;
  22 +
  23 +class OpenApiControllerTest {
  24 +
  25 + private final DeliveryFeeService deliveryFeeService = mock(DeliveryFeeService.class);
  26 + private final OpenAppService openAppService = mock(OpenAppService.class);
  27 + private final AmapRouteClient amapRouteClient = mock(AmapRouteClient.class);
  28 + private final OpenApiController controller =
  29 + new OpenApiController(deliveryFeeService, openAppService, amapRouteClient);
  30 +
  31 + @Test
  32 + void routeCalcShouldReturnAmapValuesWhenHit() {
  33 + OpenRouteCalcDTO dto = dto("121.4737", "31.2304", "121.5067", "31.2454");
  34 + when(amapRouteClient.getElectrobikeRoute(
  35 + dto.getStartLng(), dto.getStartLat(), dto.getEndLng(), dto.getEndLat()))
  36 + .thenReturn(Optional.of(new AmapRouteClient.AmapRoute(4830, 1080)));
  37 +
  38 + Result<RouteCalcVO> result = controller.routeCalc(dto);
  39 +
  40 + assertEquals(0, result.getCode());
  41 + assertEquals(4830, result.getData().getDistance());
  42 + assertEquals(1080, result.getData().getDuration());
  43 + }
  44 +
  45 + @Test
  46 + void routeCalcShouldFallbackToGeoUtilWhenAmapMisses() {
  47 + OpenRouteCalcDTO dto = dto("121.4737", "31.2304", "121.4737", "31.2574");
  48 + when(amapRouteClient.getElectrobikeRoute(any(), any(), any(), any()))
  49 + .thenReturn(Optional.empty());
  50 +
  51 + Result<RouteCalcVO> result = controller.routeCalc(dto);
  52 +
  53 + int expected = (int) Math.round(
  54 + GeoUtil.calcDistanceKm(31.2304, 121.4737, 31.2574, 121.4737) * 1000);
  55 + assertEquals(0, result.getCode());
  56 + assertNotNull(result.getData().getDistance());
  57 + assertEquals(expected, result.getData().getDistance());
  58 + assertNull(result.getData().getDuration());
  59 + }
  60 +
  61 + @Test
  62 + void routeCalcShouldThrowOnInvalidCoordinatesWhenAmapMisses() {
  63 + OpenRouteCalcDTO dto = dto("not-a-number", "31.2304", "121.5067", "31.2454");
  64 + when(amapRouteClient.getElectrobikeRoute(any(), any(), any(), any()))
  65 + .thenReturn(Optional.empty());
  66 +
  67 + assertThrows(BizException.class, () -> controller.routeCalc(dto));
  68 + }
  69 +
  70 + private OpenRouteCalcDTO dto(String startLng, String startLat, String endLng, String endLat) {
  71 + OpenRouteCalcDTO dto = new OpenRouteCalcDTO();
  72 + dto.setStartLng(startLng);
  73 + dto.setStartLat(startLat);
  74 + dto.setEndLng(endLng);
  75 + dto.setEndLat(endLat);
  76 + return dto;
  77 + }
  78 +}
... ...
src/test/java/com/diligrp/rider/service/impl/DeliveryFeeServiceImplTest.java
... ... @@ -4,6 +4,7 @@ import com.diligrp.rider.dto.DeliveryPricingConfigDTO;
4 4 import com.diligrp.rider.dto.DeliveryPricingRuleDTO;
5 5 import com.diligrp.rider.dto.DeliveryFeeCalcDTO;
6 6 import com.diligrp.rider.service.CityService;
  7 +import com.diligrp.rider.service.amap.AmapRouteClient;
7 8 import com.diligrp.rider.util.GeoUtil;
8 9 import com.diligrp.rider.vo.DeliveryFeeResultVO;
9 10 import org.junit.jupiter.api.Test;
... ... @@ -14,13 +15,19 @@ import java.time.LocalDateTime;
14 15 import java.time.ZoneId;
15 16 import java.util.Arrays;
16 17 import java.util.List;
  18 +import java.util.Optional;
17 19  
18 20 import static org.junit.jupiter.api.Assertions.assertEquals;
  21 +import static org.mockito.ArgumentMatchers.any;
19 22 import static org.mockito.Mockito.mock;
  23 +import static org.mockito.Mockito.never;
  24 +import static org.mockito.Mockito.verify;
  25 +import static org.mockito.Mockito.when;
20 26  
21 27 class DeliveryFeeServiceImplTest {
22 28  
23   - private final DeliveryFeeServiceImpl service = new DeliveryFeeServiceImpl(mock(CityService.class));
  29 + private final AmapRouteClient amapRouteClient = mock(AmapRouteClient.class);
  30 + private final DeliveryFeeServiceImpl service = new DeliveryFeeServiceImpl(mock(CityService.class), amapRouteClient);
24 31  
25 32 @Test
26 33 void shouldApplyMinFee() {
... ... @@ -126,6 +133,52 @@ class DeliveryFeeServiceImplTest {
126 133 assertEquals(new BigDecimal("2.50"), result.getTotalFee());
127 134 }
128 135  
  136 + @Test
  137 + void shouldUseAmapDurationWhenAvailable() {
  138 + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig();
  139 + pricingConfig.getType6().setDistanceSwitch(0);
  140 + pricingConfig.getType6().setWeightSwitch(0);
  141 +
  142 + DeliveryFeeCalcDTO calc = baseCalc();
  143 + calc.setEndLat("31.2574");
  144 + calc.setEndLng("121.4737");
  145 + when(amapRouteClient.getElectrobikeDurationMinutes(
  146 + calc.getStartLng(), calc.getStartLat(), calc.getEndLng(), calc.getEndLat()))
  147 + .thenReturn(Optional.of(18));
  148 +
  149 + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, calc);
  150 +
  151 + assertEquals(18, result.getEstimatedMinutes());
  152 + }
  153 +
  154 + @Test
  155 + void shouldFallbackToFormulaWhenAmapEmpty() {
  156 + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig();
  157 + pricingConfig.getType6().setDistanceSwitch(0);
  158 + pricingConfig.getType6().setWeightSwitch(0);
  159 +
  160 + when(amapRouteClient.getElectrobikeDurationMinutes(any(), any(), any(), any()))
  161 + .thenReturn(Optional.empty());
  162 +
  163 + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, baseCalc());
  164 +
  165 + // distanceKm=0 时低于 distance_basic=3,应取基础时长 distance_basic_time=30
  166 + assertEquals(30, result.getEstimatedMinutes());
  167 + }
  168 +
  169 + @Test
  170 + void shouldNotCallAmapForFixedFee() {
  171 + DeliveryPricingConfigDTO pricingConfig = defaultPricingConfig();
  172 + pricingConfig.getType6().setFeeMode(1);
  173 + pricingConfig.getType6().setFixMoney(new BigDecimal("8.00"));
  174 +
  175 + DeliveryFeeResultVO result = service.calcFeeByConfig(pricingConfig, baseCalc());
  176 +
  177 + assertEquals(new BigDecimal("8.00"), result.getTotalFee());
  178 + assertEquals(30, result.getEstimatedMinutes());
  179 + verify(amapRouteClient, never()).getElectrobikeDurationMinutes(any(), any(), any(), any());
  180 + }
  181 +
129 182 private DeliveryPricingConfigDTO defaultPricingConfig() {
130 183 DeliveryPricingConfigDTO config = new DeliveryPricingConfigDTO();
131 184 config.setType(Arrays.asList(6));
... ...