|
|
@@ -3,14 +3,21 @@ package com.ruoyi.app.flashdelivery.service;
|
|
|
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressRequest;
|
|
|
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressChangeRequest;
|
|
|
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressConfirmRequest;
|
|
|
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryTipAddRequest;
|
|
|
+import com.ruoyi.app.flashdelivery.dto.FlashDeliveryQuoteView;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryCreateRequest;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryProofRequest;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryPricingRequest;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryQuoteRequest;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryDeliverRequest;
|
|
|
import com.ruoyi.app.flashdelivery.dto.FlashDeliveryRiderOrderPageView;
|
|
|
+import com.ruoyi.app.flashdelivery.exception.FlashDeliveryQuoteChangedException;
|
|
|
import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
|
|
|
import com.ruoyi.app.flashdelivery.route.RouteDistance;
|
|
|
+import com.ruoyi.app.order.RiderDeliveryExclusivityService;
|
|
|
+import com.ruoyi.app.order.RiderDeliveryLockService;
|
|
|
import com.ruoyi.system.domain.InfoUser;
|
|
|
import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
|
|
|
import com.ruoyi.system.domain.flash.FlashDeliveryOrderImage;
|
|
|
@@ -37,15 +44,18 @@ import org.springframework.test.util.ReflectionTestUtils;
|
|
|
import java.math.BigDecimal;
|
|
|
import java.util.List;
|
|
|
import java.util.Date;
|
|
|
+import java.util.Calendar;
|
|
|
|
|
|
import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.ACCEPTED;
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
import com.ruoyi.common.exception.ServiceException;
|
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyString;
|
|
|
import static org.mockito.ArgumentMatchers.eq;
|
|
|
import static org.mockito.ArgumentMatchers.isNull;
|
|
|
import static org.mockito.Mockito.mock;
|
|
|
@@ -55,6 +65,7 @@ import static org.mockito.Mockito.verify;
|
|
|
import static org.mockito.Mockito.when;
|
|
|
import static org.mockito.Mockito.doAnswer;
|
|
|
import static org.mockito.Mockito.inOrder;
|
|
|
+import static org.mockito.Mockito.doThrow;
|
|
|
|
|
|
class FlashDeliveryApplicationServiceTest {
|
|
|
private static ConfigurableListableBeanFactory originalBeanFactory;
|
|
|
@@ -78,7 +89,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
@Test
|
|
|
void quoteUsesServerRouteAndCurrentTimePricing() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectCurrent("HELP_SEND")).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
|
|
|
when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3001, 600, "ROUTE"));
|
|
|
|
|
|
var quote = fixture.service.quote(request());
|
|
|
@@ -94,7 +105,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
@Test
|
|
|
void quoteRejectsRoutesLongerThanFortyKilometres() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectCurrent("HELP_SEND")).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
|
|
|
when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(40001, 1800, "ROUTE"));
|
|
|
|
|
|
assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
|
|
|
@@ -153,9 +164,11 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void scheduledCreateGeneratesUserOnlyPinAndStoresSenderProof() {
|
|
|
+ void scheduledCreateGeneratesUserOnlyPinAndStoresRelativeSenderProof() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectCurrent("HELP_SEND")).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
|
|
|
+ .thenReturn(List.of(88L));
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
|
|
|
when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
doAnswer(invocation -> {
|
|
|
FlashDeliveryOrder order = invocation.getArgument(0);
|
|
|
@@ -165,6 +178,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
FlashDeliveryCreateRequest request = createRequest();
|
|
|
+ request.setSenderImageUrls(List.of("/profile/upload/2026/09/07/sender.jpg"));
|
|
|
|
|
|
var result = fixture.service.create(7L, request);
|
|
|
|
|
|
@@ -174,10 +188,16 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
verify(fixture.imageMapper).insert(image.capture());
|
|
|
assertEquals("SENDER", image.getValue().getProofType());
|
|
|
assertEquals("USER", image.getValue().getOperatorType());
|
|
|
+ assertEquals("/profile/upload/2026/09/07/sender.jpg", image.getValue().getImageUrl());
|
|
|
ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
verify(fixture.orderMapper).insert(saved.capture());
|
|
|
assertEquals("SCHEDULED", saved.getValue().getDeliveryMode());
|
|
|
+ assertEquals("NORMAL", saved.getValue().getDeliveryType());
|
|
|
assertEquals("DOCUMENT", saved.getValue().getPackageType());
|
|
|
+ assertEquals(1, saved.getValue().getQuantity());
|
|
|
+ assertEquals("UP_TO_5_KG", saved.getValue().getWeightRange());
|
|
|
+ assertEquals("30 x 20 x 10 cm", saved.getValue().getSpecification());
|
|
|
+ assertEquals(88L, saved.getValue().getReceiverUserId());
|
|
|
assertEquals(1L, saved.getValue().getPricingId());
|
|
|
assertEquals("00:00", saved.getValue().getPricingStartTime());
|
|
|
assertEquals("24:00", saved.getValue().getPricingEndTime());
|
|
|
@@ -186,6 +206,11 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
assertEquals(new BigDecimal("1.00"), saved.getValue().getDistance());
|
|
|
assertEquals(15L, saved.getValue().getFreight());
|
|
|
assertEquals(0L, saved.getValue().getDistanceFee());
|
|
|
+ assertEquals(90L, saved.getValue().getBaseDeliveryFee());
|
|
|
+ assertEquals(new BigDecimal("20.00"), saved.getValue().getUrgentRate());
|
|
|
+ assertEquals(10L, saved.getValue().getMinimumUrgentFee());
|
|
|
+ assertEquals(0L, saved.getValue().getUrgentFee());
|
|
|
+ assertEquals(0L, saved.getValue().getTipAmount());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -193,12 +218,13 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
void userListOnlyFiltersByOwnerAndReturnsCardFields() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
+ order.setWeightRange("OVER_5_TO_10_KG");
|
|
|
Page<FlashDeliveryOrder> source = new Page<>(1, 10, 1);
|
|
|
source.setRecords(List.of(order));
|
|
|
when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
any(Wrapper.class))).thenReturn(source);
|
|
|
|
|
|
- var result = fixture.service.userOrders(7L, 1, 10);
|
|
|
+ var result = fixture.service.userOrders(7L, 1, 10, null);
|
|
|
|
|
|
ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
|
|
|
ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
|
|
|
@@ -210,15 +236,75 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
assertFalse(sql.contains("service_type"));
|
|
|
assertTrue(parameters.contains(7L));
|
|
|
assertEquals("pickup road", result.getRecords().get(0).getPickupAddress());
|
|
|
+ assertEquals("OVER_5_TO_10_KG", result.getRecords().get(0).getWeightRange());
|
|
|
assertEquals(112L, result.getRecords().get(0).getAmount());
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ void createDoesNotBindReceiverWhenNormalizedPhoneMatchesMultipleUsers() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
+ when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("886912345678"))
|
|
|
+ .thenReturn(List.of(88L, 99L));
|
|
|
+ doAnswer(invocation -> {
|
|
|
+ FlashDeliveryOrder order = invocation.getArgument(0);
|
|
|
+ order.setId(99L);
|
|
|
+ return 1;
|
|
|
+ }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
|
|
|
+ FlashDeliveryCreateRequest request = createRequest();
|
|
|
+ request.getDelivery().setPhone("+886 912-345-678");
|
|
|
+
|
|
|
+ fixture.service.create(7L, request);
|
|
|
+
|
|
|
+ ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
+ verify(fixture.orderMapper).insert(saved.capture());
|
|
|
+ assertNull(saved.getValue().getReceiverUserId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createLeavesReceiverUnboundWhenPhoneHasNoRegisteredUser() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
+ when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
|
|
|
+ .thenReturn(List.of());
|
|
|
+ doAnswer(invocation -> {
|
|
|
+ FlashDeliveryOrder order = invocation.getArgument(0);
|
|
|
+ order.setId(99L);
|
|
|
+ return 1;
|
|
|
+ }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
|
|
|
+
|
|
|
+ fixture.service.create(7L, createRequest());
|
|
|
+
|
|
|
+ ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
+ verify(fixture.orderMapper).insert(saved.capture());
|
|
|
+ assertNull(saved.getValue().getReceiverUserId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ void receiverListFiltersByCreationTimeReceiverBinding() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
+ any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
+
|
|
|
+ fixture.service.userOrders(88L, 1, 10, "receiver");
|
|
|
+
|
|
|
+ ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
|
|
|
+ ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
|
|
|
+ verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
|
|
|
+ assertTrue(query.getValue().getSqlSegment().toLowerCase().contains("receiver_user_id"));
|
|
|
+ assertTrue(query.getValue().getParamNameValuePairs().values().contains(88L));
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
void riderNewTaskUsesFoodOrderParametersAndReturnsPrototypeCardSummary() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
rider(fixture, 8L);
|
|
|
FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
|
|
|
+ order.setWeightRange("OVER_10_TO_15_KG");
|
|
|
Page<FlashDeliveryOrder> source = new Page<>(1, 10, 1);
|
|
|
source.setRecords(List.of(order));
|
|
|
when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
@@ -239,6 +325,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
assertEquals(168L, result.getHighestOrderAmount());
|
|
|
assertEquals("pickup road", result.getRecords().get(0).getPickupAddress());
|
|
|
assertEquals("delivery road", result.getRecords().get(0).getDeliveryAddress());
|
|
|
+ assertEquals("OVER_10_TO_15_KG", result.getRecords().get(0).getWeightRange());
|
|
|
assertTrue(result.getRecords().get(0).getPickupDistanceMeters() > 0);
|
|
|
assertTrue(result.getRecords().get(0).getPinRequired());
|
|
|
ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
|
|
|
@@ -392,6 +479,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
Fixture fixture = new Fixture();
|
|
|
rider(fixture, 8L);
|
|
|
FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
|
|
|
+ order.setWeightRange("OVER_15_TO_20_KG");
|
|
|
when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
|
|
|
var result = fixture.service.riderDetail(8L, 10L);
|
|
|
@@ -401,6 +489,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
assertEquals("pickup door", result.getPickup().getAddressDetail());
|
|
|
assertEquals("delivery road", result.getDelivery().getAddress());
|
|
|
assertEquals("delivery door", result.getDelivery().getAddressDetail());
|
|
|
+ assertEquals("OVER_15_TO_20_KG", result.getWeightRange());
|
|
|
assertNull(result.getPickup().getName());
|
|
|
assertNull(result.getPickup().getPhone());
|
|
|
assertNull(result.getPickup().getLongitude());
|
|
|
@@ -423,6 +512,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
Fixture fixture = new Fixture();
|
|
|
FlashDeliveryOrder order = order(10L, ACCEPTED);
|
|
|
order.setRiderId(null);
|
|
|
+ order.setWeightRange("OVER_5_TO_10_KG");
|
|
|
when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of(
|
|
|
image("SENDER", "https://example.com/sender.jpg"),
|
|
|
@@ -436,9 +526,40 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
assertEquals(List.of("https://example.com/pickup.jpg"), result.getPickupImageUrls());
|
|
|
assertEquals(List.of("https://example.com/delivery.jpg"), result.getDeliveryImageUrls());
|
|
|
assertEquals("4821", result.getDeliveryPinCode());
|
|
|
+ assertEquals("OVER_5_TO_10_KG", result.getWeightRange());
|
|
|
verify(fixture.logMapper, never()).selectList(any());
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ void matchedReceiverCanViewAndConfirmButCannotCancelOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "DELIVERED");
|
|
|
+ order.setReceiverUserId(88L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ when(fixture.orderMapper.transitionByParticipant(eq(10L), eq(88L), eq("DELIVERED"),
|
|
|
+ eq("COMPLETED"), any(Date.class))).thenReturn(1);
|
|
|
+ when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
+
|
|
|
+ assertDoesNotThrow(() -> fixture.service.userDetail(88L, 10L));
|
|
|
+ assertDoesNotThrow(() -> fixture.service.confirmReceipt(88L, 10L));
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.userCancel(88L, 10L, new com.ruoyi.app.flashdelivery.dto.FlashDeliveryReasonRequest()));
|
|
|
+ verify(fixture.orderMapper).transitionByParticipant(eq(10L), eq(88L), eq("DELIVERED"),
|
|
|
+ eq("COMPLETED"), any(Date.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void orderWithoutCreationTimeReceiverBindingIsNotClaimedLater() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder historicalOrder = order(10L, "DELIVERED");
|
|
|
+ historicalOrder.setReceiverUserId(null);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(historicalOrder);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.userDetail(88L, 10L));
|
|
|
+
|
|
|
+ verify(fixture.userMapper, never()).selectOrdinaryUserIdsByNormalizedPhone(any());
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
void pickupRejectsMissingProofBeforeChangingState() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
@@ -459,11 +580,43 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
InfoUser rider = new InfoUser();
|
|
|
rider.setUserId(8L);
|
|
|
rider.setUserType("2");
|
|
|
+ rider.setDeliveryType("FLASH");
|
|
|
when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
|
|
|
when(fixture.orderMapper.accept(eq(10L), eq(8L), any())).thenReturn(0);
|
|
|
|
|
|
assertThrows(RuntimeException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
- verify(fixture.orderMapper, never()).selectById(10L);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void flashAcceptUsesRiderLockAndChecksExclusivityBeforeAtomicUpdate() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ FlashDeliveryOrder waiting = order(10L, "WAITING_ACCEPTANCE");
|
|
|
+ FlashDeliveryOrder accepted = order(10L, ACCEPTED);
|
|
|
+ accepted.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(waiting, accepted);
|
|
|
+ when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(1);
|
|
|
+
|
|
|
+ fixture.service.accept(8L, 10L);
|
|
|
+
|
|
|
+ InOrder order = inOrder(fixture.lockService, fixture.exclusivityService, fixture.orderMapper);
|
|
|
+ order.verify(fixture.lockService).withLock(eq(8L), any(RiderDeliveryLockService.LockedCall.class));
|
|
|
+ order.verify(fixture.exclusivityService).assertCanAcceptFlash(8L, "NORMAL");
|
|
|
+ order.verify(fixture.orderMapper).accept(eq(10L), eq(8L), any(Date.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void flashExclusiveConflictDoesNotUpdateOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
|
|
|
+ doThrow(new ServiceException("exclusive"))
|
|
|
+ .when(fixture.exclusivityService).assertCanAcceptFlash(8L, "NORMAL");
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
+
|
|
|
+ verify(fixture.orderMapper, never()).accept(any(), any(), any());
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -501,7 +654,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
void concurrentPricingUpdateIsRejected() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
when(fixture.pricingMapper.selectById(1L)).thenReturn(pricing());
|
|
|
- when(fixture.pricingMapper.countOverlapping("HELP_SEND", "00:00", "12:00", 1L)).thenReturn(0);
|
|
|
+ when(fixture.pricingMapper.countOverlapping("00:00", "12:00", 1L)).thenReturn(0);
|
|
|
when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
|
|
|
FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
request.setEndTime("12:00");
|
|
|
@@ -516,7 +669,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
void overlappingPricingPeriodIsRejectedBeforeInsert() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
- when(fixture.pricingMapper.countOverlapping("HELP_SEND", "00:00", "24:00", null)).thenReturn(1);
|
|
|
+ when(fixture.pricingMapper.countOverlapping("00:00", "24:00", null)).thenReturn(1);
|
|
|
|
|
|
assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
|
|
|
|
|
|
@@ -554,7 +707,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
void createPricingPersistsImmediatelyActiveTimePeriod() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
- when(fixture.pricingMapper.countOverlapping("HELP_SEND", "00:00", "24:00", null)).thenReturn(0);
|
|
|
+ when(fixture.pricingMapper.countOverlapping("00:00", "24:00", null)).thenReturn(0);
|
|
|
|
|
|
fixture.service.createPricing(7L, request);
|
|
|
|
|
|
@@ -565,9 +718,8 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
assertEquals(90L, saved.getValue().getStartingFare());
|
|
|
|
|
|
InOrder writeOrder = inOrder(fixture.pricingMapper);
|
|
|
- writeOrder.verify(fixture.pricingMapper).lockServiceType("HELP_SEND");
|
|
|
writeOrder.verify(fixture.pricingMapper)
|
|
|
- .countOverlapping("HELP_SEND", "00:00", "24:00", null);
|
|
|
+ .countOverlapping("00:00", "24:00", null);
|
|
|
writeOrder.verify(fixture.pricingMapper).insert(any(FlashDeliveryPricing.class));
|
|
|
}
|
|
|
|
|
|
@@ -590,7 +742,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
@Test
|
|
|
void multipleCurrentPricingPeriodsFailExplicitlyBeforeRouteLookup() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectCurrent("HELP_SEND"))
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString()))
|
|
|
.thenReturn(List.of(pricing(), pricing()));
|
|
|
|
|
|
assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
|
|
|
@@ -599,10 +751,9 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- void movingPricingPeriodLocksOldAndNewServiceTypesInStableOrder() {
|
|
|
+ void updatingPricingChecksOverlapBeforeVersionedWrite() {
|
|
|
Fixture fixture = new Fixture();
|
|
|
FlashDeliveryPricing existing = pricing();
|
|
|
- existing.setServiceType("URGENT");
|
|
|
when(fixture.pricingMapper.selectById(1L)).thenReturn(existing);
|
|
|
when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
|
|
|
FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
@@ -610,11 +761,10 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
|
|
|
fixture.service.updatePricing(9L, 1L, request);
|
|
|
|
|
|
- InOrder lockOrder = inOrder(fixture.pricingMapper);
|
|
|
- lockOrder.verify(fixture.pricingMapper).lockServiceType("HELP_SEND");
|
|
|
- lockOrder.verify(fixture.pricingMapper).lockServiceType("URGENT");
|
|
|
- lockOrder.verify(fixture.pricingMapper)
|
|
|
- .countOverlapping("HELP_SEND", "00:00", "24:00", 1L);
|
|
|
+ InOrder writeOrder = inOrder(fixture.pricingMapper);
|
|
|
+ writeOrder.verify(fixture.pricingMapper)
|
|
|
+ .countOverlapping("00:00", "24:00", 1L);
|
|
|
+ writeOrder.verify(fixture.pricingMapper).update(isNull(), any(Wrapper.class));
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -625,18 +775,133 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
|
|
|
fixture.service.deletePricing(18L);
|
|
|
|
|
|
- InOrder deleteOrder = inOrder(fixture.pricingMapper);
|
|
|
- deleteOrder.verify(fixture.pricingMapper).selectById(18L);
|
|
|
- deleteOrder.verify(fixture.pricingMapper).lockServiceType("HELP_SEND");
|
|
|
- deleteOrder.verify(fixture.pricingMapper).selectById(18L);
|
|
|
- deleteOrder.verify(fixture.pricingMapper).deleteById(18L);
|
|
|
+ verify(fixture.pricingMapper).deleteById(18L);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void urgentQuoteReturnsSeparatedFeesAndUsesScheduledPickupTime() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime("16:30")).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(6000, 900, "ROUTE"));
|
|
|
+ FlashDeliveryQuoteRequest request = request();
|
|
|
+ request.setDeliveryType("URGENT");
|
|
|
+ request.setTipAmount(5L);
|
|
|
+ request.setDeliveryMode("SCHEDULED");
|
|
|
+ Calendar calendar = Calendar.getInstance();
|
|
|
+ calendar.add(Calendar.DAY_OF_MONTH, 1);
|
|
|
+ calendar.set(Calendar.HOUR_OF_DAY, 16);
|
|
|
+ calendar.set(Calendar.MINUTE, 30);
|
|
|
+ calendar.set(Calendar.SECOND, 0);
|
|
|
+ calendar.set(Calendar.MILLISECOND, 0);
|
|
|
+ request.setScheduledPickupStartAt(calendar.getTime());
|
|
|
+ request.setScheduledPickupEndAt(new Date(calendar.getTimeInMillis() + 30L * 60L * 1000L));
|
|
|
+
|
|
|
+ var quote = fixture.service.quote(request);
|
|
|
+
|
|
|
+ assertEquals(45L, quote.getDistanceFee());
|
|
|
+ assertEquals(135L, quote.getBaseDeliveryFee());
|
|
|
+ assertEquals(27L, quote.getUrgentFee());
|
|
|
+ assertEquals(5L, quote.getTipAmount());
|
|
|
+ assertEquals(167L, quote.getAmount());
|
|
|
+ verify(fixture.pricingMapper).selectAtTime("16:30");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createRejectsChangedQuoteAndReturnsLatestQuoteWithoutInsert() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
+ FlashDeliveryCreateRequest request = createRequest();
|
|
|
+ request.setQuotedAmount(91L);
|
|
|
+
|
|
|
+ FlashDeliveryQuoteChangedException error = assertThrows(FlashDeliveryQuoteChangedException.class,
|
|
|
+ () -> fixture.service.create(7L, request));
|
|
|
+
|
|
|
+ assertEquals(90L, error.getLatestQuote().getAmount());
|
|
|
+ assertEquals(1L, error.getLatestQuote().getPricingId());
|
|
|
+ verify(fixture.orderMapper, never()).insert(any(FlashDeliveryOrder.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void quoteValidatesDeliveryLevelAndCompleteItemInformation() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryQuoteRequest request = request();
|
|
|
+ request.setDeliveryType("FAST");
|
|
|
+ FlashDeliveryQuoteRequest invalidDeliveryType = request;
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quote(invalidDeliveryType));
|
|
|
+
|
|
|
+ request = request();
|
|
|
+ request.setQuantity(0);
|
|
|
+ FlashDeliveryQuoteRequest invalidQuantity = request;
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quote(invalidQuantity));
|
|
|
+
|
|
|
+ request = request();
|
|
|
+ request.setWeightRange("SMALL");
|
|
|
+ FlashDeliveryQuoteRequest oldWeightRange = request;
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quote(oldWeightRange));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void quoteAcceptsAllFourWeightRanges() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
+
|
|
|
+ for (String weightRange : List.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
|
|
|
+ "OVER_10_TO_15_KG", "OVER_15_TO_20_KG")) {
|
|
|
+ FlashDeliveryQuoteRequest request = request();
|
|
|
+ request.setWeightRange(weightRange);
|
|
|
+ var quote = assertDoesNotThrow(() -> fixture.service.quote(request), weightRange);
|
|
|
+ assertEquals(90L, quote.getAmount(), weightRange);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createPersistsAndReturnsEveryWeightRange() {
|
|
|
+ for (String weightRange : List.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
|
|
|
+ "OVER_10_TO_15_KG", "OVER_15_TO_20_KG")) {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
+ doAnswer(invocation -> {
|
|
|
+ FlashDeliveryOrder order = invocation.getArgument(0);
|
|
|
+ order.setId(99L);
|
|
|
+ return 1;
|
|
|
+ }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
|
|
|
+ when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
+ when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
+ FlashDeliveryCreateRequest request = createRequest();
|
|
|
+ request.setWeightRange(weightRange);
|
|
|
+
|
|
|
+ var result = fixture.service.create(7L, request);
|
|
|
+
|
|
|
+ ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
+ verify(fixture.orderMapper).insert(saved.capture());
|
|
|
+ assertEquals(weightRange, saved.getValue().getWeightRange());
|
|
|
+ assertEquals(weightRange, result.getWeightRange());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void quoteRejectsMissingAndUnknownWeightRanges() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ for (String weightRange : new String[]{null, "", "SMALL", "OVER_20_KG"}) {
|
|
|
+ FlashDeliveryQuoteRequest request = request();
|
|
|
+ request.setWeightRange(weightRange);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quote(request), String.valueOf(weightRange));
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private FlashDeliveryQuoteRequest request() {
|
|
|
FlashDeliveryQuoteRequest request = new FlashDeliveryQuoteRequest();
|
|
|
request.setServiceType("HELP_SEND");
|
|
|
+ request.setDeliveryType("NORMAL");
|
|
|
request.setPackageType("DOCUMENT");
|
|
|
- request.setPackageSize("SMALL");
|
|
|
+ request.setQuantity(1);
|
|
|
+ request.setWeightRange("UP_TO_5_KG");
|
|
|
+ request.setSpecification("30 x 20 x 10 cm");
|
|
|
+ request.setTipAmount(0L);
|
|
|
+ request.setDeliveryMode("NOW");
|
|
|
request.setPickup(address("A", "0911111111", "25.0330", "121.5645"));
|
|
|
request.setDelivery(address("B", "0922222222", "25.0478", "121.5319"));
|
|
|
request.getDelivery().setAddress("second road");
|
|
|
@@ -647,8 +912,12 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
FlashDeliveryCreateRequest request = new FlashDeliveryCreateRequest();
|
|
|
FlashDeliveryQuoteRequest quote = request();
|
|
|
request.setServiceType(quote.getServiceType());
|
|
|
+ request.setDeliveryType(quote.getDeliveryType());
|
|
|
request.setPackageType(quote.getPackageType());
|
|
|
- request.setPackageSize(quote.getPackageSize());
|
|
|
+ request.setQuantity(quote.getQuantity());
|
|
|
+ request.setWeightRange(quote.getWeightRange());
|
|
|
+ request.setSpecification(quote.getSpecification());
|
|
|
+ request.setTipAmount(quote.getTipAmount());
|
|
|
request.setPickup(quote.getPickup());
|
|
|
request.setDelivery(quote.getDelivery());
|
|
|
request.setClientRequestId("flash-scheduled-001");
|
|
|
@@ -657,6 +926,12 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
request.setScheduledPickupStartAt(start);
|
|
|
request.setScheduledPickupEndAt(new Date(start.getTime() + 30L * 60L * 1000L));
|
|
|
request.setPinRequired(true);
|
|
|
+ request.setPricingId(1L);
|
|
|
+ request.setPricingVersion(1);
|
|
|
+ request.setQuotedBaseDeliveryFee(90L);
|
|
|
+ request.setQuotedDistanceFee(0L);
|
|
|
+ request.setQuotedUrgentFee(0L);
|
|
|
+ request.setQuotedAmount(90L);
|
|
|
request.setSenderImageUrls(List.of("https://example.com/sender.jpg"));
|
|
|
return request;
|
|
|
}
|
|
|
@@ -675,26 +950,28 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
private FlashDeliveryPricing pricing() {
|
|
|
FlashDeliveryPricing pricing = new FlashDeliveryPricing();
|
|
|
pricing.setId(1L);
|
|
|
- pricing.setServiceType("HELP_SEND");
|
|
|
pricing.setStartTime("00:00");
|
|
|
pricing.setEndTime("24:00");
|
|
|
pricing.setStartingDistance(new BigDecimal("3.00"));
|
|
|
pricing.setStartingFare(90L);
|
|
|
pricing.setDistance(new BigDecimal("1.00"));
|
|
|
pricing.setFreight(15L);
|
|
|
+ pricing.setUrgentRate(new BigDecimal("20.00"));
|
|
|
+ pricing.setMinimumUrgentFee(10L);
|
|
|
pricing.setConfigVersion(1);
|
|
|
return pricing;
|
|
|
}
|
|
|
|
|
|
private FlashDeliveryPricingRequest pricingRequest() {
|
|
|
FlashDeliveryPricingRequest request = new FlashDeliveryPricingRequest();
|
|
|
- request.setServiceType("HELP_SEND");
|
|
|
request.setStartTime("00:00");
|
|
|
request.setEndTime("24:00");
|
|
|
request.setStartingDistance(new BigDecimal("3.00"));
|
|
|
request.setStartingFare(90L);
|
|
|
request.setDistance(new BigDecimal("1.00"));
|
|
|
request.setFreight(15L);
|
|
|
+ request.setUrgentRate(new BigDecimal("20.00"));
|
|
|
+ request.setMinimumUrgentFee(10L);
|
|
|
return request;
|
|
|
}
|
|
|
|
|
|
@@ -722,6 +999,7 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
InfoUser rider = new InfoUser();
|
|
|
rider.setUserId(riderId);
|
|
|
rider.setUserType("2");
|
|
|
+ rider.setDeliveryType("FLASH");
|
|
|
when(fixture.userMapper.selectInfoUserByUserId(riderId)).thenReturn(rider);
|
|
|
}
|
|
|
|
|
|
@@ -731,9 +1009,11 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
order.setOrderNo("FL-20260813-021");
|
|
|
order.setUserId(7L);
|
|
|
order.setServiceType("HELP_SEND");
|
|
|
+ order.setDeliveryType("NORMAL");
|
|
|
order.setStatus(status);
|
|
|
order.setPackageType("DOCUMENT");
|
|
|
- order.setPackageSize("SMALL");
|
|
|
+ order.setQuantity(1);
|
|
|
+ order.setWeightRange("UP_TO_5_KG");
|
|
|
order.setDeliveryMode("NOW");
|
|
|
order.setPinRequired(true);
|
|
|
order.setDeliveryPinCode("4821");
|
|
|
@@ -755,6 +1035,12 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
order.setDeliveryLatitude(new BigDecimal("25.0200"));
|
|
|
order.setDistanceMeters(3200);
|
|
|
order.setEstimatedDurationSeconds(2100);
|
|
|
+ order.setDistanceFee(12L);
|
|
|
+ order.setBaseDeliveryFee(102L);
|
|
|
+ order.setUrgentRate(new BigDecimal("20.00"));
|
|
|
+ order.setMinimumUrgentFee(10L);
|
|
|
+ order.setUrgentFee(0L);
|
|
|
+ order.setTipAmount(10L);
|
|
|
order.setAmount(112L);
|
|
|
order.setCurrency("TWD");
|
|
|
order.setUserNote("private note");
|
|
|
@@ -769,6 +1055,303 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
return image;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void missingOrInvalidBodyOrderIdIsRejectedBeforeDatabaseAccess() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ for (Long orderId : java.util.Arrays.asList(null, 0L, -1L)) {
|
|
|
+ ServiceException addressFailure = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, orderId, new FlashDeliveryAddressConfirmRequest()));
|
|
|
+ ServiceException tipFailure = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, orderId, tipRequest(20L, 2)));
|
|
|
+ assertEquals("flash.delivery.order.not.found", addressFailure.getMessage());
|
|
|
+ assertEquals("flash.delivery.order.not.found", tipFailure.getMessage());
|
|
|
+ }
|
|
|
+ org.mockito.Mockito.verifyNoInteractions(fixture.orderMapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressQuoteUsesOrderPricingSnapshotAndDoesNotWrite() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ // 已到预约时间也允许待接单订单改地址,不重新验证创建时的预约窗口。
|
|
|
+ order.setDeliveryMode("SCHEDULED");
|
|
|
+ order.setScheduledPickupStartAt(new Date(1));
|
|
|
+ order.setScheduledPickupEndAt(new Date(1800001));
|
|
|
+ FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, addressChange());
|
|
|
+ assertEquals(4500, quote.getDistanceMeters());
|
|
|
+ assertEquals(900, quote.getEstimatedDurationSeconds());
|
|
|
+ assertEquals(23L, quote.getDistanceFee());
|
|
|
+ assertEquals(113L, quote.getBaseDeliveryFee());
|
|
|
+ assertEquals(10L, quote.getTipAmount());
|
|
|
+ assertEquals(123L, quote.getAmount());
|
|
|
+ assertEquals(2, quote.getOrderVersion());
|
|
|
+ assertEquals(1, quote.getPricingVersion());
|
|
|
+ assertEquals("pickup road", order.getPickupAddress());
|
|
|
+ verify(fixture.pricingMapper, never()).selectAtTime(anyString());
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
|
|
|
+ verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressSaveUpdatesBothEndsRouteFeesAndReceiverTogether() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
|
|
|
+ .thenReturn(List.of(88L));
|
|
|
+ when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
|
|
|
+ FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
|
|
|
+ input.getPickup().setName(" new sender ");
|
|
|
+ input.getPickup().setAddressDetail(null);
|
|
|
+ input.getDelivery().setName("new receiver");
|
|
|
+ var view = fixture.service.updateAddress(7L, 10L, input);
|
|
|
+ assertEquals("new sender", view.getPickup().getName());
|
|
|
+ assertEquals("new receiver", view.getDelivery().getName());
|
|
|
+ assertNull(view.getPickup().getAddressDetail());
|
|
|
+ assertEquals(4500, view.getDistanceMeters());
|
|
|
+ assertEquals(123L, view.getAmount());
|
|
|
+ assertEquals(3, view.getOrderVersion());
|
|
|
+ assertEquals(88L, order.getReceiverUserId());
|
|
|
+ assertEquals(10L, order.getTipAmount());
|
|
|
+ assertEquals("WAITING_ACCEPTANCE", order.getStatus());
|
|
|
+ verify(fixture.orderMapper).updateWaitingAddress(order, 2);
|
|
|
+ verify(fixture.logMapper).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressChangeClearsFormerReceiverWhenNewPhoneDoesNotMatch() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ order.setReceiverUserId(88L);
|
|
|
+ when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
|
|
|
+ fixture.service.updateAddress(7L, 10L, confirmAddress(fixture));
|
|
|
+ assertNull(order.getReceiverUserId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressQuotePreservesUrgentRulesAndExistingTip() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ order.setDeliveryType("URGENT");
|
|
|
+ var quote = fixture.service.quoteAddress(7L, 10L, addressChange());
|
|
|
+ assertEquals(23L, quote.getUrgentFee());
|
|
|
+ assertEquals(146L, quote.getAmount());
|
|
|
+ assertEquals(10L, quote.getTipAmount());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void changedRouteOrUnconfirmedPriceReturnsLatestQuoteWithoutSaving() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
|
|
|
+ input.setQuotedAmount(1L);
|
|
|
+ var exception = assertThrows(FlashDeliveryQuoteChangedException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, 10L, input));
|
|
|
+ assertEquals(123L, exception.getLatestQuote().getAmount());
|
|
|
+ input.setQuotedAmount(123L);
|
|
|
+ input.setQuotedDistanceMeters(4499);
|
|
|
+ assertThrows(FlashDeliveryQuoteChangedException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, 10L, input));
|
|
|
+ input.setQuotedDistanceMeters(4500);
|
|
|
+ input.setQuotedUrgentFee(null);
|
|
|
+ assertThrows(FlashDeliveryQuoteChangedException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, 10L, input));
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
|
|
|
+ verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void receiverAndOtherUsersCannotQuoteEditOrAddTip() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ order.setReceiverUserId(88L);
|
|
|
+ for (Long user : List.of(88L, 99L)) {
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(user, 10L, addressChange()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.updateAddress(user, 10L, new FlashDeliveryAddressConfirmRequest()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(user, 10L, tipRequest(5L, 2)));
|
|
|
+ }
|
|
|
+ verify(fixture.routeService, never()).calculate(any(), any());
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void everyNonWaitingStateAndAssignedRiderRejectsBothChanges() {
|
|
|
+ for (String status : List.of("ACCEPTED", "PICKED_UP", "DELIVERED", "COMPLETED", "CANCELLED")) {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ order.setStatus(status);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, new FlashDeliveryAddressConfirmRequest()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
|
|
|
+ }
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture).setRiderId(8L);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void staleOrMissingVersionCannotOverwriteNewerOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressChangeRequest input = addressChange();
|
|
|
+ input.setOrderVersion(1);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
|
|
|
+ for (Integer version : java.util.Arrays.asList(null, -1, 1)) {
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, 10L, tipRequest(5L, version)));
|
|
|
+ }
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addressValidationRejectsSameInvalidAndOverDistanceEndpoints() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressChangeRequest input = addressChange();
|
|
|
+ input.setDelivery(input.getPickup());
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
|
|
|
+ input.setDelivery(request().getDelivery());
|
|
|
+ input.getPickup().setPhone("");
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(40001, 1000, "GOOGLE"));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void riderAcceptanceBetweenReadAndAddressSaveRejectsWriteWithoutLog() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(0);
|
|
|
+ ServiceException exception = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.updateAddress(7L, 10L, input));
|
|
|
+ assertEquals("flash.delivery.state.changed", exception.getMessage());
|
|
|
+ verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void addingTipOnlyIncreasesTipAndTotalAndRejectsSameVersionRetry() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
|
|
|
+ var view = fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
|
|
|
+ assertEquals(30L, view.getTipAmount());
|
|
|
+ assertEquals(132L, view.getAmount());
|
|
|
+ assertEquals(102L, view.getBaseDeliveryFee());
|
|
|
+ assertEquals(3200, view.getDistanceMeters());
|
|
|
+ assertEquals(3, view.getOrderVersion());
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
|
|
|
+ assertEquals(30L, order.getTipAmount());
|
|
|
+ verify(fixture.orderMapper).updateWaitingTip(any(), eq(2));
|
|
|
+ verify(fixture.routeService, never()).calculate(any(), any());
|
|
|
+ verify(fixture.pricingMapper, never()).selectAtTime(anyString());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void tipChangeInvalidatesEarlierAddressConfirmation() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryAddressConfirmRequest confirmation = confirmAddress(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
|
|
|
+ fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, confirmation));
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void additionalTipRejectsFractionalJsonBeforeWriting() throws Exception {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
|
|
|
+ FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
|
|
|
+ "{\"orderVersion\":2,\"additionalTipAmount\":1.9}", FlashDeliveryTipAddRequest.class);
|
|
|
+ ServiceException exception = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, 10L, input));
|
|
|
+ assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void additionalTipRejectsJsonAmountBeyondLongRange() throws Exception {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
|
|
|
+ "{\"orderVersion\":2,\"additionalTipAmount\":1e100}", FlashDeliveryTipAddRequest.class);
|
|
|
+ ServiceException exception = assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, 10L, input));
|
|
|
+ assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void additionalTipRejectsZeroNegativeNullAndOverflow() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ for (Long value : java.util.Arrays.asList(null, 0L, -1L, Long.MAX_VALUE)) {
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.addTip(7L, 10L, tipRequest(value, 2)));
|
|
|
+ }
|
|
|
+ verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void riderAcceptanceBetweenReadAndTipSaveRejectsWriteWithoutLog() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(0);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
|
|
|
+ verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryOrder editableOrder(Fixture fixture) {
|
|
|
+ FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
|
|
|
+ order.setRiderId(null);
|
|
|
+ order.setVersion(2);
|
|
|
+ order.setPricingId(1L);
|
|
|
+ order.setPricingVersion(1);
|
|
|
+ order.setPricingStartTime("00:00");
|
|
|
+ order.setPricingEndTime("24:00");
|
|
|
+ order.setStartingDistance(new BigDecimal("3.00"));
|
|
|
+ order.setStartingFare(90L);
|
|
|
+ order.setDistance(new BigDecimal("1.00"));
|
|
|
+ order.setFreight(15L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(4500, 900, "GOOGLE"));
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryAddressChangeRequest addressChange() {
|
|
|
+ FlashDeliveryAddressChangeRequest input = new FlashDeliveryAddressChangeRequest();
|
|
|
+ input.setOrderVersion(2);
|
|
|
+ input.setPickup(request().getPickup());
|
|
|
+ input.setDelivery(request().getDelivery());
|
|
|
+ return input;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryAddressConfirmRequest confirmAddress(Fixture fixture) {
|
|
|
+ FlashDeliveryAddressChangeRequest change = addressChange();
|
|
|
+ FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, change);
|
|
|
+ FlashDeliveryAddressConfirmRequest input = new FlashDeliveryAddressConfirmRequest();
|
|
|
+ input.setOrderVersion(quote.getOrderVersion());
|
|
|
+ input.setPickup(change.getPickup());
|
|
|
+ input.setDelivery(change.getDelivery());
|
|
|
+ input.setQuotedDistanceMeters(quote.getDistanceMeters());
|
|
|
+ input.setQuotedBaseDeliveryFee(quote.getBaseDeliveryFee());
|
|
|
+ input.setQuotedDistanceFee(quote.getDistanceFee());
|
|
|
+ input.setQuotedUrgentFee(quote.getUrgentFee());
|
|
|
+ input.setQuotedAmount(quote.getAmount());
|
|
|
+ return input;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryTipAddRequest tipRequest(Long amount, Integer version) {
|
|
|
+ FlashDeliveryTipAddRequest input = new FlashDeliveryTipAddRequest();
|
|
|
+ input.setAdditionalTipAmount(amount == null ? null : BigDecimal.valueOf(amount));
|
|
|
+ input.setOrderVersion(version);
|
|
|
+ return input;
|
|
|
+ }
|
|
|
+
|
|
|
private static class Fixture {
|
|
|
final FlashDeliveryOrderMapper orderMapper = mock(FlashDeliveryOrderMapper.class);
|
|
|
final FlashDeliveryPricingMapper pricingMapper = mock(FlashDeliveryPricingMapper.class);
|
|
|
@@ -776,13 +1359,17 @@ class FlashDeliveryApplicationServiceTest {
|
|
|
final FlashDeliveryOrderLogMapper logMapper = mock(FlashDeliveryOrderLogMapper.class);
|
|
|
final InfoUserMapper userMapper = mock(InfoUserMapper.class);
|
|
|
final FlashDeliveryRouteService routeService = mock(FlashDeliveryRouteService.class);
|
|
|
+ final RiderDeliveryLockService lockService = mock(RiderDeliveryLockService.class);
|
|
|
+ final RiderDeliveryExclusivityService exclusivityService = mock(RiderDeliveryExclusivityService.class);
|
|
|
final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(
|
|
|
orderMapper, pricingMapper, imageMapper, logMapper, userMapper,
|
|
|
- routeService, new FlashDeliveryPricingCalculator());
|
|
|
+ routeService, new FlashDeliveryPricingCalculator(), lockService, exclusivityService);
|
|
|
|
|
|
Fixture() {
|
|
|
- when(pricingMapper.lockServiceType(any()))
|
|
|
- .thenAnswer(invocation -> invocation.getArgument(0));
|
|
|
+ when(lockService.withLock(any(), any())).thenAnswer(invocation -> {
|
|
|
+ RiderDeliveryLockService.LockedCall<?> call = invocation.getArgument(1);
|
|
|
+ return call.call();
|
|
|
+ });
|
|
|
}
|
|
|
}
|
|
|
}
|