|
|
@@ -1,2070 +1,2159 @@
|
|
|
-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.FlashDeliveryReasonRequest;
|
|
|
-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.GeoPoint;
|
|
|
-import com.ruoyi.app.flashdelivery.route.RouteDistance;
|
|
|
-import com.ruoyi.app.order.RiderDeliveryAcceptanceService;
|
|
|
-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;
|
|
|
-import com.ruoyi.system.domain.flash.FlashDeliveryPricing;
|
|
|
-import com.ruoyi.system.mapper.InfoUserMapper;
|
|
|
-import com.ruoyi.system.mapper.flash.FlashDeliveryOrderImageMapper;
|
|
|
-import com.ruoyi.system.mapper.flash.FlashDeliveryOrderLogMapper;
|
|
|
-import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
|
|
|
-import com.ruoyi.system.mapper.flash.FlashDeliveryPricingMapper;
|
|
|
-import org.junit.jupiter.api.Test;
|
|
|
-import org.junit.jupiter.api.BeforeAll;
|
|
|
-import org.junit.jupiter.api.AfterAll;
|
|
|
-import org.mockito.ArgumentCaptor;
|
|
|
-import org.mockito.InOrder;
|
|
|
-import org.mockito.MockedStatic;
|
|
|
-import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
|
|
-import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
|
|
-import org.springframework.context.support.StaticMessageSource;
|
|
|
-import com.ruoyi.common.core.domain.entity.SysDictData;
|
|
|
-import com.ruoyi.common.utils.DictUtils;
|
|
|
-import com.ruoyi.common.utils.spring.SpringUtils;
|
|
|
-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.assertNotEquals;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
-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.anyInt;
|
|
|
-import static org.mockito.ArgumentMatchers.anyString;
|
|
|
-import static org.mockito.ArgumentMatchers.eq;
|
|
|
-import static org.mockito.ArgumentMatchers.isNull;
|
|
|
-import static org.mockito.Mockito.mock;
|
|
|
-import static org.mockito.Mockito.mockStatic;
|
|
|
-import static org.mockito.Mockito.never;
|
|
|
-import static org.mockito.Mockito.times;
|
|
|
-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;
|
|
|
-import static org.mockito.Mockito.verifyNoInteractions;
|
|
|
-
|
|
|
-class FlashDeliveryApplicationServiceTest {
|
|
|
- private static ConfigurableListableBeanFactory originalBeanFactory;
|
|
|
-
|
|
|
- @BeforeAll
|
|
|
- static void initializeMessages() {
|
|
|
- originalBeanFactory = (ConfigurableListableBeanFactory)
|
|
|
- ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
|
|
|
- DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
|
|
- StaticMessageSource messageSource = new StaticMessageSource();
|
|
|
- messageSource.setUseCodeAsDefaultMessage(true);
|
|
|
- beanFactory.registerSingleton("messageSource", messageSource);
|
|
|
- new SpringUtils().postProcessBeanFactory(beanFactory);
|
|
|
- }
|
|
|
-
|
|
|
- @AfterAll
|
|
|
- static void restoreMessages() {
|
|
|
- new SpringUtils().postProcessBeanFactory(originalBeanFactory);
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void quoteUsesServerRouteAndCurrentTimePricing() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3001, 600, "ROUTE"));
|
|
|
-
|
|
|
- var quote = fixture.service.quote(request());
|
|
|
-
|
|
|
- assertEquals(3001, quote.getDistanceMeters());
|
|
|
- assertEquals(90L, quote.getAmount());
|
|
|
- assertEquals(1L, quote.getPricingId());
|
|
|
- assertEquals("00:00", quote.getStartTime());
|
|
|
- assertEquals("24:00", quote.getEndTime());
|
|
|
- assertEquals("ROUTE", quote.getDistanceSource());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void quoteRejectsRoutesLongerThanFortyKilometres() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(40001, 1800, "ROUTE"));
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void wrongDeliveryPinCannotPersistProofOrChangeState() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setUserType("2");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
- order.setId(10L);
|
|
|
- order.setRiderId(8L);
|
|
|
- order.setStatus("PICKED_UP");
|
|
|
- order.setPinRequired(true);
|
|
|
- order.setDeliveryPinCode("4821");
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
- FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
|
|
|
- request.setPinCode("1234");
|
|
|
- request.setImageUrls(List.of("https://example.com/delivery.jpg"));
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.deliver(8L, 10L, request));
|
|
|
- verify(fixture.imageMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class));
|
|
|
- verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void correctDeliveryPinPersistsDeliveryProofAndChangesState() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setUserType("2");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
- order.setId(10L);
|
|
|
- order.setRiderId(8L);
|
|
|
- order.setStatus("PICKED_UP");
|
|
|
- order.setPinRequired(true);
|
|
|
- order.setDeliveryPinCode("4821");
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
- when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("PICKED_UP"),
|
|
|
- eq("DELIVERED"), eq("delivered_at"), any())).thenReturn(1);
|
|
|
- FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
|
|
|
- request.setPinCode("4821");
|
|
|
- request.setImageUrls(List.of("https://example.com/delivery.jpg"));
|
|
|
-
|
|
|
- fixture.service.deliver(8L, 10L, request);
|
|
|
-
|
|
|
- ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
|
|
|
- ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
|
|
|
- verify(fixture.imageMapper).insert(image.capture());
|
|
|
- assertEquals("DELIVERY", image.getValue().getProofType());
|
|
|
- assertEquals("RIDER", image.getValue().getOperatorType());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void scheduledCreateGeneratesUserOnlyPinAndStoresRelativeSenderProof() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
|
|
|
- .thenReturn(List.of(88L));
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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.setSenderImageUrls(List.of("/profile/upload/2026/09/07/sender.jpg"));
|
|
|
-
|
|
|
- var result = fixture.service.create(7L, request);
|
|
|
-
|
|
|
- assertEquals(4, result.getDeliveryPinCode().length());
|
|
|
- ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
|
|
|
- ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
|
|
|
- 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());
|
|
|
- assertEquals(new BigDecimal("3.00"), saved.getValue().getStartingDistance());
|
|
|
- assertEquals(90L, saved.getValue().getStartingFare());
|
|
|
- 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
|
|
|
- void confirmPaymentRequiresDeliveredState() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 9L);
|
|
|
- FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
|
|
|
- pickedUp.setRiderId(9L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.confirmPayment(9L, 10L));
|
|
|
- verify(fixture.orderMapper, never()).update(any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void confirmPaymentMarksPaidAndWritesLog() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 9L);
|
|
|
- FlashDeliveryOrder delivered = order(10L, "DELIVERED");
|
|
|
- delivered.setRiderId(9L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(delivered);
|
|
|
- when(fixture.orderMapper.update(any(), any())).thenReturn(1);
|
|
|
- when(fixture.logMapper.insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class))).thenReturn(1);
|
|
|
-
|
|
|
- fixture.service.confirmPayment(9L, 10L);
|
|
|
-
|
|
|
- verify(fixture.orderMapper).update(any(), any());
|
|
|
- ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderLog> logCaptor =
|
|
|
- ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class);
|
|
|
- verify(fixture.logMapper).insert(logCaptor.capture());
|
|
|
- assertEquals("PAYMENT_CONFIRMED", logCaptor.getValue().getReason());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void confirmPaymentIsIdempotentOnceConfirmed() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 9L);
|
|
|
- FlashDeliveryOrder confirmed = order(10L, "DELIVERED");
|
|
|
- confirmed.setRiderId(9L);
|
|
|
- confirmed.setPaymentStatus(1);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(confirmed);
|
|
|
-
|
|
|
- fixture.service.confirmPayment(9L, 10L);
|
|
|
-
|
|
|
- verify(fixture.orderMapper, never()).update(any(), any());
|
|
|
- verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
- 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, null);
|
|
|
-
|
|
|
- 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());
|
|
|
- String sql = query.getValue().getSqlSegment().toLowerCase();
|
|
|
- var parameters = query.getValue().getParamNameValuePairs().values();
|
|
|
- assertTrue(sql.contains("user_id"));
|
|
|
- assertFalse(sql.contains("status"));
|
|
|
- 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(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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),
|
|
|
- any(Wrapper.class))).thenReturn(source);
|
|
|
- when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of(168L));
|
|
|
-
|
|
|
- SysDictData distanceLimit = new SysDictData();
|
|
|
- distanceLimit.setDictValue("5");
|
|
|
- FlashDeliveryRiderOrderPageView result;
|
|
|
- try (MockedStatic<DictUtils> dictionary = mockStatic(DictUtils.class)) {
|
|
|
- dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance"))
|
|
|
- .thenReturn(List.of(distanceLimit));
|
|
|
- result = fixture.service.riderOrders(8L, 1, 10, "newTask",
|
|
|
- new BigDecimal("121.5000"), new BigDecimal("25.0000"));
|
|
|
- }
|
|
|
-
|
|
|
- assertEquals(1L, result.getNearbyTaskCount());
|
|
|
- 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 =
|
|
|
- ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
|
|
|
- verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
|
|
|
- String sql = query.getValue().getSqlSegment().toLowerCase();
|
|
|
- var parameters = query.getValue().getParamNameValuePairs().values();
|
|
|
- assertTrue(sql.contains("status"));
|
|
|
- assertTrue(sql.contains("rider_id is null"));
|
|
|
- assertTrue(sql.contains("is_display"));
|
|
|
- assertTrue(sql.contains("vehicle_type"));
|
|
|
- assertTrue(sql.contains("scheduled_pickup_start_at"));
|
|
|
- assertTrue(sql.contains("scheduled_pickup_end_at"));
|
|
|
- assertTrue(sql.contains("st_distance_sphere"));
|
|
|
- assertTrue(parameters.contains("WAITING_ACCEPTANCE"));
|
|
|
- assertTrue(parameters.contains("NOW"));
|
|
|
- assertTrue(parameters.stream().anyMatch(Date.class::isInstance));
|
|
|
-
|
|
|
- ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> summaryQuery =
|
|
|
- ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
|
|
|
- verify(fixture.orderMapper).selectObjs(summaryQuery.capture());
|
|
|
- String summarySql = summaryQuery.getValue().getSqlSegment().toLowerCase();
|
|
|
- var summaryParameters = summaryQuery.getValue().getParamNameValuePairs().values();
|
|
|
- assertTrue(summarySql.contains("rider_id is null"));
|
|
|
- assertTrue(summarySql.contains("scheduled_pickup_start_at"));
|
|
|
- assertTrue(summarySql.contains("scheduled_pickup_end_at"));
|
|
|
- assertTrue(summarySql.contains("st_distance_sphere"));
|
|
|
- assertTrue(summaryParameters.contains("WAITING_ACCEPTANCE"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
- void riderNewTaskWithoutPositionFallsBackToNewestFirst() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
- any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
- when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of());
|
|
|
-
|
|
|
- fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
|
|
|
-
|
|
|
- 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());
|
|
|
- String sql = query.getValue().getSqlSegment().toLowerCase();
|
|
|
- assertTrue(sql.contains("order by create_time desc"));
|
|
|
- assertFalse(sql.contains("st_distance_sphere"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
- void riderTabsMapStatusesBeforePagingAndRestrictAssignedTabsToRider() {
|
|
|
- assertRiderTab("toPickup", List.of("ACCEPTED"));
|
|
|
- assertRiderTab("delivering", List.of("PICKED_UP"));
|
|
|
- assertRiderTab("completed", List.of("DELIVERED", "COMPLETED"));
|
|
|
- assertRiderTab("cancelled", List.of("CANCELLED"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void riderListRejectsUnknownTabAndUnpairedOrInvalidCoordinates() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class,
|
|
|
- () -> fixture.service.riderOrders(8L, 1, 10, "refund", null, null));
|
|
|
- assertThrows(ServiceException.class,
|
|
|
- () -> fixture.service.riderOrders(8L, 1, 10, null, null, null));
|
|
|
- assertThrows(ServiceException.class,
|
|
|
- () -> fixture.service.riderOrders(8L, 1, 10, "newTask", BigDecimal.ONE, null));
|
|
|
- assertThrows(ServiceException.class,
|
|
|
- () -> fixture.service.riderOrders(8L, 1, 10, "newTask",
|
|
|
- new BigDecimal("181"), BigDecimal.ZERO));
|
|
|
- verify(fixture.orderMapper, never()).selectPage(any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void flashNewTaskListAndAcceptRequireFlashDeliveryType() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setUserType("2");
|
|
|
- rider.setDeliveryType("FOOD");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class,
|
|
|
- () -> fixture.service.riderOrders(8L, 1, 10, "newTask", null, null));
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
- verify(fixture.orderMapper, never()).selectPage(any(), any());
|
|
|
- verify(fixture.orderMapper, never()).accept(any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void riderDetailHidesWaitingFlashOrdersFromNonFlashRiders() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setUserType("2");
|
|
|
- rider.setDeliveryType("FOOD");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.riderDetail(8L, 10L));
|
|
|
- verify(fixture.imageMapper, never()).selectList(any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
- void enabledFlashRiderStillListsNewTasks() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setUserType("2");
|
|
|
- rider.setDeliveryType("FLASH");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
- any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
-
|
|
|
- fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
|
|
|
- verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
- any(Wrapper.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
- void ownOrderTabsRemainAccessibleRegardlessOfDeliveryType() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setUserType("2");
|
|
|
- rider.setDeliveryType("FOOD");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
- any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
-
|
|
|
- fixture.service.riderOrders(8L, 1, 10, "toPickup", null, null);
|
|
|
- verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
- any(Wrapper.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void assignedRiderTabsIgnoreLocationParameters() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
- any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
-
|
|
|
- fixture.service.riderOrders(8L, 1, 10, "toPickup",
|
|
|
- new BigDecimal("181"), new BigDecimal("91"));
|
|
|
-
|
|
|
- verify(fixture.orderMapper).selectPage(any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void preAcceptRiderDetailIsDirectAndShowsTextAddressWithoutSensitiveFields() {
|
|
|
- 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);
|
|
|
-
|
|
|
- assertEquals(10L, result.getId());
|
|
|
- assertEquals("pickup road", result.getPickup().getAddress());
|
|
|
- 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());
|
|
|
- assertNull(result.getPickup().getLatitude());
|
|
|
- assertNull(result.getDelivery().getName());
|
|
|
- assertNull(result.getDelivery().getPhone());
|
|
|
- assertNull(result.getDelivery().getLongitude());
|
|
|
- assertNull(result.getDelivery().getLatitude());
|
|
|
- assertNull(result.getDeliveryPinCode());
|
|
|
- assertNull(result.getUserNote());
|
|
|
- assertNull(result.getUser());
|
|
|
- assertTrue(result.getSenderImageUrls().isEmpty());
|
|
|
- assertTrue(result.getPickupImageUrls().isEmpty());
|
|
|
- assertTrue(result.getDeliveryImageUrls().isEmpty());
|
|
|
- verify(fixture.userMapper, never()).selectInfoUserByUserId(7L);
|
|
|
- verify(fixture.imageMapper, never()).selectList(any());
|
|
|
- verify(fixture.logMapper, never()).selectList(any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void userDetailReturnsDirectOrderAndGroupsImageUrlsWithoutRawLogs() {
|
|
|
- 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"),
|
|
|
- image("PICKUP", "https://example.com/pickup.jpg"),
|
|
|
- image("DELIVERY", "https://example.com/delivery.jpg")));
|
|
|
-
|
|
|
- var result = fixture.service.userDetail(7L, 10L);
|
|
|
-
|
|
|
- assertEquals(10L, result.getId());
|
|
|
- assertEquals(List.of("https://example.com/sender.jpg"), result.getSenderImageUrls());
|
|
|
- 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 userDetailReturnsRiderInfoAfterAcceptance() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, ACCEPTED);
|
|
|
- order.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setUserType("2");
|
|
|
- rider.setNickName("闪送骑士");
|
|
|
- rider.setTelPhone("0933333333");
|
|
|
- rider.setVehicleType("2");
|
|
|
- rider.setLicensePlate("ABC-1234");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
-
|
|
|
- var result = fixture.service.userDetail(7L, 10L);
|
|
|
-
|
|
|
- assertEquals(8L, result.getRider().getUserId());
|
|
|
- assertEquals("闪送骑士", result.getRider().getNickName());
|
|
|
- assertEquals("0933333333", result.getRider().getTelPhone());
|
|
|
- assertEquals("ABC-1234", result.getRider().getLicensePlate());
|
|
|
- assertNull(result.getUser());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void assignedRiderDetailReturnsSenderInfo() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- FlashDeliveryOrder order = order(10L, ACCEPTED);
|
|
|
- order.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
- InfoUser sender = new InfoUser();
|
|
|
- sender.setUserId(7L);
|
|
|
- sender.setNickName("寄件达人");
|
|
|
- sender.setPhone("0911111111");
|
|
|
- sender.setAvatar("https://example.com/avatar.png");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(7L)).thenReturn(sender);
|
|
|
-
|
|
|
- var result = fixture.service.riderDetail(8L, 10L);
|
|
|
-
|
|
|
- assertEquals(7L, result.getUser().getUserId());
|
|
|
- assertEquals("寄件达人", result.getUser().getNickName());
|
|
|
- assertEquals("0911111111", result.getUser().getPhone());
|
|
|
- assertEquals("https://example.com/avatar.png", result.getUser().getAvatar());
|
|
|
- assertNull(result.getRider());
|
|
|
- }
|
|
|
-
|
|
|
- @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 senderRatesRiderOnCompletedOrder() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
- order.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
- when(fixture.orderMapper.rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class))).thenReturn(1);
|
|
|
-
|
|
|
- fixture.service.rateRider(7L, 10L, rating(4.5));
|
|
|
-
|
|
|
- verify(fixture.orderMapper).rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void rateRiderRejectsUserOtherThanSender() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
- order.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.rateRider(88L, 10L, rating(4.5)));
|
|
|
- verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void rateRiderRejectsOrderNotCompletedOrWithoutRider() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder delivered = order(10L, "DELIVERED");
|
|
|
- delivered.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(delivered);
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(4.5)));
|
|
|
-
|
|
|
- FlashDeliveryOrder completedWithoutRider = order(11L, "COMPLETED");
|
|
|
- when(fixture.orderMapper.selectById(11L)).thenReturn(completedWithoutRider);
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 11L, rating(4.5)));
|
|
|
-
|
|
|
- verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void rateRiderRejectsAlreadyRatedOrder() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
- order.setRiderId(8L);
|
|
|
- order.setRiderStars(4.5);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(3.0)));
|
|
|
- verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void rateRiderRejectsConcurrentDuplicateSubmission() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
- order.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
- when(fixture.orderMapper.rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class))).thenReturn(0);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(4.5)));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void rateRiderRejectsMissingOrOutOfRangeStars() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, null));
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(null)));
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(0.5)));
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(5.5)));
|
|
|
-
|
|
|
- verify(fixture.orderMapper, never()).selectById(any());
|
|
|
- verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void userDetailReturnsRiderStars() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
- order.setRiderId(8L);
|
|
|
- order.setRiderStars(4.5);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
- when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
-
|
|
|
- var view = fixture.service.userDetail(7L, 10L);
|
|
|
-
|
|
|
- assertEquals(4.5, view.getRiderStars());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void userDetailRiderCarriesFlashAverageStars() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, "ACCEPTED");
|
|
|
- order.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setNickName("闪送骑手");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- when(fixture.orderMapper.selectRiderAverageStars(8L)).thenReturn(4.8);
|
|
|
- when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
-
|
|
|
- var view = fixture.service.userDetail(7L, 10L);
|
|
|
-
|
|
|
- assertEquals(4.8, view.getRider().getStar());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void userDetailRiderFallsBackToDefaultStarsWithoutRatings() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, "ACCEPTED");
|
|
|
- order.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- when(fixture.orderMapper.selectRiderAverageStars(8L)).thenReturn(null);
|
|
|
- when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
-
|
|
|
- var view = fixture.service.userDetail(7L, 10L);
|
|
|
-
|
|
|
- assertEquals(4.5, view.getRider().getStar());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void userDetailByOrderNoReturnsParticipantDetail() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
- order.setRiderId(8L);
|
|
|
- order.setRiderStars(4.5);
|
|
|
- when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
-
|
|
|
- var view = fixture.service.userDetailByOrderNo(7L, "FL-20260813-021");
|
|
|
-
|
|
|
- assertEquals(10L, view.getId());
|
|
|
- assertEquals(4.5, view.getRiderStars());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void userDetailByOrderNoRejectsNonParticipantAndUnknownOrderNo() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
- order.setReceiverUserId(null);
|
|
|
- when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
|
|
|
- assertThrows(ServiceException.class,
|
|
|
- () -> fixture.service.userDetailByOrderNo(88L, "FL-20260813-021"));
|
|
|
-
|
|
|
- when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(null);
|
|
|
- assertThrows(ServiceException.class,
|
|
|
- () -> fixture.service.userDetailByOrderNo(7L, "FL-UNKNOWN"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void riderDetailByOrderNoReturnsAssignedOrderDetail() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- FlashDeliveryOrder order = order(10L, "ACCEPTED");
|
|
|
- order.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(7L)).thenReturn(new InfoUser());
|
|
|
- when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
-
|
|
|
- var view = fixture.service.riderDetailByOrderNo(8L, "FL-20260813-021");
|
|
|
-
|
|
|
- assertEquals(10L, view.getId());
|
|
|
- assertNotNull(view.getUser());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void riderDetailByOrderNoRejectsUnknownOrderNo() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(null);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class,
|
|
|
- () -> fixture.service.riderDetailByOrderNo(8L, "FL-UNKNOWN"));
|
|
|
- }
|
|
|
-
|
|
|
- @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();
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setUserType("2");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
|
|
|
- request.setImageUrls(List.of());
|
|
|
-
|
|
|
- assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
|
|
|
- verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void atomicAcceptFailureDoesNotBindSecondRider() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- 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));
|
|
|
- }
|
|
|
-
|
|
|
- // 预约单已过取件结束时间:即使骑手绕过列表直接持有订单 ID 请求接单,锁内校验也必须拒绝。
|
|
|
- @Test
|
|
|
- void expiredScheduledOrderCannotBeAcceptedEvenWhenItsIdIsKnown() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
|
|
|
- Date now = new Date();
|
|
|
- order.setDeliveryMode("SCHEDULED");
|
|
|
- order.setScheduledPickupStartAt(new Date(now.getTime() - 31L * 60L * 1000L));
|
|
|
- order.setScheduledPickupEndAt(new Date(now.getTime() - 60L * 1000L));
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
- verify(fixture.orderMapper, never()).accept(any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void flashAcceptUsesRiderLockAndChecksAcceptancePolicyBeforeAtomicUpdate() {
|
|
|
- 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.acceptanceService, fixture.orderMapper);
|
|
|
- order.verify(fixture.lockService).withLock(eq(8L), any(RiderDeliveryLockService.LockedCall.class));
|
|
|
- order.verify(fixture.acceptanceService).assertCanAcceptFlash(8L, "NORMAL");
|
|
|
- order.verify(fixture.orderMapper).accept(eq(10L), eq(8L), any(Date.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void flashAcceptanceConflictDoesNotUpdateOrder() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
|
|
|
- doThrow(new ServiceException("exclusive"))
|
|
|
- .when(fixture.acceptanceService).assertCanAcceptFlash(8L, "NORMAL");
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
-
|
|
|
- verify(fixture.orderMapper, never()).accept(any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void acceptSuccessNotifiesSenderOfOrder() {
|
|
|
- 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);
|
|
|
-
|
|
|
- verify(fixture.notificationService).notifyAccepted(waiting);
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void acceptFailureDoesNotTriggerNotification() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
|
|
|
- when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(0);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
-
|
|
|
- verify(fixture.notificationService, never()).notifyAccepted(any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void pickupSuccessNotifiesSenderOfOrder() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- FlashDeliveryOrder accepted = order(10L, ACCEPTED);
|
|
|
- accepted.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
|
|
|
- // 逐站取件走 updateFulfilment 条件更新而非 transitionByRider。
|
|
|
- when(fixture.orderMapper.updateFulfilment(any())).thenReturn(1);
|
|
|
- FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
|
|
|
- request.setImageUrls(List.of("https://example.com/pickup.jpg"));
|
|
|
-
|
|
|
- fixture.service.pickup(8L, 10L, request);
|
|
|
-
|
|
|
- verify(fixture.notificationService).notifyPickedUp(accepted);
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void deliverSuccessNotifiesSenderOfOrder() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
|
|
|
- pickedUp.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
|
|
|
- when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("PICKED_UP"),
|
|
|
- eq("DELIVERED"), eq("delivered_at"), any())).thenReturn(1);
|
|
|
- FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
|
|
|
- request.setPinCode("4821");
|
|
|
- request.setImageUrls(List.of("https://example.com/delivery.jpg"));
|
|
|
-
|
|
|
- fixture.service.deliver(8L, 10L, request);
|
|
|
-
|
|
|
- verify(fixture.notificationService).notifyDelivered(pickedUp);
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void proofTransitionFailureDoesNotTriggerNotification() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- FlashDeliveryOrder accepted = order(10L, ACCEPTED);
|
|
|
- accepted.setRiderId(8L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
|
|
|
- when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("ACCEPTED"),
|
|
|
- eq("PICKED_UP"), eq("picked_up_at"), any())).thenReturn(0);
|
|
|
- FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
|
|
|
- request.setImageUrls(List.of("https://example.com/pickup.jpg"));
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.pickup(8L, 10L, request));
|
|
|
-
|
|
|
- verify(fixture.notificationService, never()).notifyPickedUp(any());
|
|
|
- verify(fixture.notificationService, never()).notifyDelivered(any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void userCancelSuccessNotifiesAssignedRider() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder accepted = order(10L, ACCEPTED);
|
|
|
- accepted.setRiderId(9L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
|
|
|
- when(fixture.orderMapper.cancel(eq(10L), eq("ACCEPTED"), eq("USER"), eq(7L),
|
|
|
- anyString(), any(Date.class))).thenReturn(1);
|
|
|
-
|
|
|
- fixture.service.userCancel(7L, 10L, reason("改行程"));
|
|
|
-
|
|
|
- verify(fixture.notificationService).notifyCancelled(accepted);
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void adminCancelSuccessNotifiesAssignedRider() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
|
|
|
- pickedUp.setRiderId(9L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
|
|
|
- // 平台异常取消先写介入审计,再走条件取消。
|
|
|
- when(fixture.orderMapper.update(any(), any())).thenReturn(1);
|
|
|
- when(fixture.orderMapper.cancel(eq(10L), eq("PICKED_UP"), eq("ADMIN"), eq(1L),
|
|
|
- anyString(), any(Date.class))).thenReturn(1);
|
|
|
- FlashDeliveryReasonRequest request = reason("异常收口");
|
|
|
- request.setProblemStopId(101L);
|
|
|
- request.setHandlingResult("平台介入收口");
|
|
|
- // 已取货订单必须记录物品去向和交接凭证。
|
|
|
- request.setPickedGoodsDisposition("已取物品交回寄件人");
|
|
|
- request.setHandoffImageUrls(List.of("https://example.com/handoff.jpg"));
|
|
|
-
|
|
|
- fixture.service.adminCancel(1L, 10L, request);
|
|
|
-
|
|
|
- verify(fixture.notificationService).notifyCancelled(pickedUp);
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void cancelFailureDoesNotTriggerNotification() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder accepted = order(10L, ACCEPTED);
|
|
|
- accepted.setRiderId(9L);
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
|
|
|
- when(fixture.orderMapper.cancel(eq(10L), eq("ACCEPTED"), eq("USER"), eq(7L),
|
|
|
- anyString(), any(Date.class))).thenReturn(0);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.userCancel(7L, 10L, reason("改行程")));
|
|
|
-
|
|
|
- verify(fixture.notificationService, never()).notifyCancelled(any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void excludedEventsNeverTriggerNotification() {
|
|
|
- // 加小费、完成、过期预约自动取消三类事件不产生任何推送。
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- editableOrder(fixture);
|
|
|
- when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
|
|
|
- fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
|
|
|
-
|
|
|
- // 合并后的 complete 要求订单已送达且全部取货站完成。
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "DELIVERED"));
|
|
|
- when(fixture.orderMapper.transitionByStatus(eq(10L), eq("DELIVERED"), eq("COMPLETED"),
|
|
|
- eq("completed_at"), any(Date.class))).thenReturn(1);
|
|
|
- assertTrue(fixture.service.complete(1L, "ADMIN", 10L));
|
|
|
-
|
|
|
- when(fixture.orderMapper.cancelExpiredScheduled(eq(10L), any(Date.class))).thenReturn(1);
|
|
|
- assertTrue(fixture.service.cancelExpiredScheduledOrder(10L, new Date()));
|
|
|
-
|
|
|
- verifyNoInteractions(fixture.notificationService);
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void immediateOrderIsVisibleAndNotifiesAvailableRiders() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- prepareCreate(fixture);
|
|
|
- FlashDeliveryCreateRequest request = createRequest();
|
|
|
- request.setDeliveryMode("NOW");
|
|
|
- request.setScheduledPickupStartAt(null);
|
|
|
- request.setScheduledPickupEndAt(null);
|
|
|
-
|
|
|
- fixture.service.create(7L, request);
|
|
|
-
|
|
|
- ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
- verify(fixture.orderMapper).insert(saved.capture());
|
|
|
- assertTrue(saved.getValue().getIsDisplay());
|
|
|
- verify(fixture.notificationService).notifyAvailable(saved.getValue());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void scheduledOrderStartsHiddenAndDoesNotNotifyBeforeWindow() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- prepareCreate(fixture);
|
|
|
-
|
|
|
- fixture.service.create(7L, createRequest());
|
|
|
-
|
|
|
- ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
- verify(fixture.orderMapper).insert(saved.capture());
|
|
|
- assertFalse(saved.getValue().getIsDisplay());
|
|
|
- verify(fixture.notificationService, never()).notifyAvailable(any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void scheduledOrderBecomesVisibleAndNotifiesOnlyAfterAtomicOpenSucceeds() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder scheduled = order(10L, "WAITING_ACCEPTANCE");
|
|
|
- scheduled.setDeliveryMode("SCHEDULED");
|
|
|
- scheduled.setIsDisplay(false);
|
|
|
- Date now = new Date();
|
|
|
- when(fixture.orderMapper.selectById(10L)).thenReturn(scheduled);
|
|
|
- when(fixture.orderMapper.openScheduledForRiders(10L, now)).thenReturn(1);
|
|
|
-
|
|
|
- assertTrue(fixture.service.openScheduledOrderForRiders(10L, now));
|
|
|
-
|
|
|
- assertTrue(scheduled.getIsDisplay());
|
|
|
- verify(fixture.notificationService).notifyAvailable(scheduled);
|
|
|
-
|
|
|
- when(fixture.orderMapper.openScheduledForRiders(10L, now)).thenReturn(0);
|
|
|
- assertFalse(fixture.service.openScheduledOrderForRiders(10L, now));
|
|
|
- verify(fixture.notificationService).notifyAvailable(scheduled);
|
|
|
- }
|
|
|
-
|
|
|
- private FlashDeliveryReasonRequest reason(String text) {
|
|
|
- FlashDeliveryReasonRequest request = new FlashDeliveryReasonRequest();
|
|
|
- request.setReason(text);
|
|
|
- return request;
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void oversizedProofUrlIsRejectedBeforeStateTransition() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(8L);
|
|
|
- rider.setUserType("2");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
- FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
|
|
|
- request.setImageUrls(List.of("https://example.com/" + "a".repeat(1000)));
|
|
|
-
|
|
|
- assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
|
|
|
- verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void missingServiceTypeReturnsBusinessErrorInsteadOfNullPointer() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryQuoteRequest request = request();
|
|
|
- request.setServiceType(null);
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.quote(request));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void equalTextAddressesAreRejectedEvenWhenCoordinatesDiffer() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryQuoteRequest request = request();
|
|
|
- request.getDelivery().setAddress(request.getPickup().getAddress());
|
|
|
- request.getDelivery().setAddressDetail(request.getPickup().getAddressDetail());
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.quote(request));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void concurrentPricingUpdateIsRejected() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectById(1L)).thenReturn(pricing());
|
|
|
- when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("12:00"), anyInt(), eq(1L))).thenReturn(0);
|
|
|
- when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
|
|
|
- FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
- request.setEndTime("12:00");
|
|
|
- request.setConfigVersion(1);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class,
|
|
|
- () -> fixture.service.updatePricing(1L, 1L, request));
|
|
|
- verify(fixture.pricingMapper, never()).updateById(any(FlashDeliveryPricing.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void overlappingPricingPeriodIsRejectedBeforeInsert() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
- when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull())).thenReturn(1);
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
|
|
|
-
|
|
|
- request.setDistance(new BigDecimal("0.004"));
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
|
|
|
-
|
|
|
- verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void pricingDistanceHalfCentimetreNormalizesToDatabaseMinimum() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
- request.setDistance(new BigDecimal("0.005"));
|
|
|
-
|
|
|
- fixture.service.createPricing(1L, request);
|
|
|
-
|
|
|
- ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
|
|
|
- verify(fixture.pricingMapper).insert(saved.capture());
|
|
|
- assertEquals(new BigDecimal("0.01"), saved.getValue().getDistance());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void pricingDistanceThatRoundsToZeroIsRejectedBeforePersistence() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
- request.setDistance(new BigDecimal("0.001"));
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
|
|
|
-
|
|
|
- verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void createPricingPersistsImmediatelyActiveTimePeriod() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
- when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull())).thenReturn(0);
|
|
|
-
|
|
|
- fixture.service.createPricing(7L, request);
|
|
|
-
|
|
|
- ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
|
|
|
- verify(fixture.pricingMapper).insert(saved.capture());
|
|
|
- assertEquals(1, saved.getValue().getConfigVersion());
|
|
|
- assertEquals(7L, saved.getValue().getUpdatedBy());
|
|
|
- assertEquals(90L, saved.getValue().getStartingFare());
|
|
|
-
|
|
|
- InOrder writeOrder = inOrder(fixture.pricingMapper);
|
|
|
- writeOrder.verify(fixture.pricingMapper)
|
|
|
- .countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull());
|
|
|
- writeOrder.verify(fixture.pricingMapper).insert(any(FlashDeliveryPricing.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void homeOmitsServiceWithoutCurrentPricingPeriod() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
-
|
|
|
- assertTrue(fixture.service.home().getServices().isEmpty());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void quoteWithoutCurrentPricingPeriodFailsBeforeRouteLookup() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
|
|
|
-
|
|
|
- verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void multipleCurrentPricingPeriodsFailExplicitlyBeforeRouteLookup() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt()))
|
|
|
- .thenReturn(List.of(pricing(), pricing()));
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
|
|
|
-
|
|
|
- verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void updatingPricingChecksOverlapBeforeVersionedWrite() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryPricing existing = pricing();
|
|
|
- when(fixture.pricingMapper.selectById(1L)).thenReturn(existing);
|
|
|
- when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
|
|
|
- FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
- request.setConfigVersion(1);
|
|
|
-
|
|
|
- fixture.service.updatePricing(9L, 1L, request);
|
|
|
-
|
|
|
- InOrder writeOrder = inOrder(fixture.pricingMapper);
|
|
|
- writeOrder.verify(fixture.pricingMapper)
|
|
|
- .countOverlapping(eq("00:00"), eq("24:00"), anyInt(), eq(1L));
|
|
|
- writeOrder.verify(fixture.pricingMapper).update(isNull(), any(Wrapper.class));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void deletePricingRemovesConfigurationImmediately() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectById(18L)).thenReturn(pricing());
|
|
|
- when(fixture.pricingMapper.deleteById(18L)).thenReturn(1);
|
|
|
-
|
|
|
- fixture.service.deletePricing(18L);
|
|
|
-
|
|
|
- verify(fixture.pricingMapper).deleteById(18L);
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void urgentQuoteReturnsSeparatedFeesAndUsesScheduledPickupTime() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(eq("16:30"), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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());
|
|
|
- // 起步价与 45 元距离附加费分开返回,避免前端重复相加。
|
|
|
- assertEquals(90L, quote.getBaseDeliveryFee());
|
|
|
- assertEquals(27L, quote.getUrgentFee());
|
|
|
- assertEquals(5L, quote.getTipAmount());
|
|
|
- assertEquals(167L, quote.getAmount());
|
|
|
- verify(fixture.pricingMapper).selectAtTime(eq("16:30"), anyInt());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void createRejectsChangedQuoteAndReturnsLatestQuoteWithoutInsert() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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 createDefaultsPayTypeToCashAndStoresTransferSelection() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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());
|
|
|
-
|
|
|
- var defaultResult = fixture.service.create(7L, createRequest());
|
|
|
- FlashDeliveryCreateRequest transfer = createRequest();
|
|
|
- transfer.setPayType("6");
|
|
|
- var transferResult = fixture.service.create(7L, transfer);
|
|
|
-
|
|
|
- ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
- verify(fixture.orderMapper, times(2)).insert(saved.capture());
|
|
|
- assertEquals("4", saved.getAllValues().get(0).getPayType());
|
|
|
- assertEquals("6", saved.getAllValues().get(1).getPayType());
|
|
|
- assertEquals("4", defaultResult.getPayType());
|
|
|
- assertEquals("6", transferResult.getPayType());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void createRejectsUnsupportedPayType() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
- FlashDeliveryCreateRequest request = createRequest();
|
|
|
- request.setPayType("2");
|
|
|
- org.mockito.Mockito.doThrow(new ServiceException("flash.delivery.pay.type.invalid"))
|
|
|
- .when(fixture.paymentMethodGateService).assertUsable(eq("2"),
|
|
|
- eq(com.ruoyi.system.service.PaymentMethodGateService.GateScope.RIDER_FLASH), isNull(), isNull());
|
|
|
-
|
|
|
- assertThrows(ServiceException.class, () -> fixture.service.create(7L, request));
|
|
|
-
|
|
|
- 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(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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 quoteAllowsMissingWeightRange() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt()))
|
|
|
- .thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
-
|
|
|
- FlashDeliveryQuoteRequest request = request();
|
|
|
- request.setWeightRange(null);
|
|
|
-
|
|
|
- assertDoesNotThrow(() -> fixture.service.quote(request));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void quoteRejectsUnknownWeightRangeWhenProvided() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- for (String weightRange : new String[]{"", "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.setVehicleType(1);
|
|
|
- request.setPackageType("DOCUMENT");
|
|
|
- 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");
|
|
|
- return request;
|
|
|
- }
|
|
|
-
|
|
|
- private FlashDeliveryCreateRequest createRequest() {
|
|
|
- FlashDeliveryCreateRequest request = new FlashDeliveryCreateRequest();
|
|
|
- FlashDeliveryQuoteRequest quote = request();
|
|
|
- request.setServiceType(quote.getServiceType());
|
|
|
- request.setDeliveryType(quote.getDeliveryType());
|
|
|
- request.setVehicleType(quote.getVehicleType());
|
|
|
- request.setPackageType(quote.getPackageType());
|
|
|
- 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");
|
|
|
- request.setDeliveryMode("SCHEDULED");
|
|
|
- Date start = new Date(System.currentTimeMillis() + 60L * 60L * 1000L);
|
|
|
- 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;
|
|
|
- }
|
|
|
-
|
|
|
- private void prepareCreate(Fixture fixture) {
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(), any(), anyInt()))
|
|
|
- .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));
|
|
|
- }
|
|
|
-
|
|
|
- private FlashDeliveryAddressRequest address(String name, String phone, String lat, String lon) {
|
|
|
- FlashDeliveryAddressRequest address = new FlashDeliveryAddressRequest();
|
|
|
- address.setName(name);
|
|
|
- address.setPhone(phone);
|
|
|
- address.setAddress("road");
|
|
|
- address.setAddressDetail("door");
|
|
|
- address.setLatitude(new BigDecimal(lat));
|
|
|
- address.setLongitude(new BigDecimal(lon));
|
|
|
- return address;
|
|
|
- }
|
|
|
-
|
|
|
- private FlashDeliveryPricing pricing() {
|
|
|
- FlashDeliveryPricing pricing = new FlashDeliveryPricing();
|
|
|
- pricing.setId(1L);
|
|
|
- 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.setVehicleType(1);
|
|
|
- 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;
|
|
|
- }
|
|
|
-
|
|
|
- @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
- private void assertRiderTab(String tab, List<String> statuses) {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- rider(fixture, 8L);
|
|
|
- when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
- any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
-
|
|
|
- fixture.service.riderOrders(8L, 1, 10, tab, null, null);
|
|
|
-
|
|
|
- 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());
|
|
|
- // MyBatis-Plus 的条件参数在渲染 SQL 段时才写入参数表,必须先取 SQL 再断言参数。
|
|
|
- String sql = query.getValue().getSqlSegment();
|
|
|
- assertTrue(sql.contains("rider_id"), tab);
|
|
|
- var parameters = query.getValue().getParamNameValuePairs().values();
|
|
|
- assertTrue(parameters.contains(8L), tab);
|
|
|
- for (String status : statuses) assertTrue(parameters.contains(status), tab + ":" + status);
|
|
|
- }
|
|
|
-
|
|
|
- private void rider(Fixture fixture, Long riderId) {
|
|
|
- InfoUser rider = new InfoUser();
|
|
|
- rider.setUserId(riderId);
|
|
|
- rider.setUserType("2");
|
|
|
- rider.setDeliveryType("FLASH");
|
|
|
- when(fixture.userMapper.selectInfoUserByUserId(riderId)).thenReturn(rider);
|
|
|
- }
|
|
|
-
|
|
|
- private com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest rating(Double stars) {
|
|
|
- com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest request =
|
|
|
- new com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest();
|
|
|
- request.setStars(stars);
|
|
|
- return request;
|
|
|
- }
|
|
|
-
|
|
|
- private FlashDeliveryOrder order(Long id, String status) {
|
|
|
- FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
- order.setId(id);
|
|
|
- order.setPickupStopCount(1); order.setPickedUpStopCount(0); order.setVersion(0);
|
|
|
- order.setOrderNo("FL-20260813-021");
|
|
|
- order.setUserId(7L);
|
|
|
- order.setServiceType("HELP_SEND");
|
|
|
- order.setDeliveryType("NORMAL");
|
|
|
- order.setStatus(status);
|
|
|
- order.setIsDisplay(true);
|
|
|
- order.setPackageType("DOCUMENT");
|
|
|
- order.setQuantity(1);
|
|
|
- order.setWeightRange("UP_TO_5_KG");
|
|
|
- order.setDeliveryMode("NOW");
|
|
|
- order.setPinRequired(true);
|
|
|
- order.setDeliveryPinCode("4821");
|
|
|
- order.setPickupName("pickup contact");
|
|
|
- order.setPickupPhone("0911111111");
|
|
|
- order.setPickupAddress("pickup road");
|
|
|
- order.setPickupAddressDetail("pickup door");
|
|
|
- order.setPickupCity("台北市");
|
|
|
- order.setPickupArea("中山區");
|
|
|
- order.setPickupLongitude(new BigDecimal("121.5100"));
|
|
|
- order.setPickupLatitude(new BigDecimal("25.0100"));
|
|
|
- order.setDeliveryName("delivery contact");
|
|
|
- order.setDeliveryPhone("0922222222");
|
|
|
- order.setDeliveryAddress("delivery road");
|
|
|
- order.setDeliveryAddressDetail("delivery door");
|
|
|
- order.setDeliveryCity("台北市");
|
|
|
- order.setDeliveryArea("大安區");
|
|
|
- order.setDeliveryLongitude(new BigDecimal("121.5200"));
|
|
|
- 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");
|
|
|
- order.setCreateTime(new Date());
|
|
|
- return order;
|
|
|
- }
|
|
|
-
|
|
|
- private FlashDeliveryOrderImage image(String proofType, String url) {
|
|
|
- FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
|
|
|
- image.setProofType(proofType);
|
|
|
- image.setImageUrl(url);
|
|
|
- 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 addressQuoteUsesOriginalPricingSnapshotAndDoesNotWrite() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
- // FR-074/093:预约单也保留原运价快照,平台调价不得改变旧单的计价参数。
|
|
|
- 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(90L, 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(), anyInt());
|
|
|
- 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(GeoPoint.class), any(GeoPoint.class), anyInt());
|
|
|
- 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(GeoPoint.class), any(GeoPoint.class), anyInt())).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(GeoPoint.class), any(GeoPoint.class), anyInt());
|
|
|
- verify(fixture.pricingMapper, never()).selectAtTime(anyString(), anyInt());
|
|
|
- }
|
|
|
-
|
|
|
- @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(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(4500, 900, "GOOGLE"));
|
|
|
- // 后台现价与快照不同;改址仍以订单保存的价格计算。
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- 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;
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void generateOrderNoUsesTimestampAndSequenceFormat() {
|
|
|
- String first = FlashDeliveryApplicationService.generateOrderNo();
|
|
|
- String second = FlashDeliveryApplicationService.generateOrderNo();
|
|
|
-
|
|
|
- assertTrue(isFlashOrderNo(first), "应为 98 前缀 17 位纯数字,实际: " + first);
|
|
|
- assertTrue(isFlashOrderNo(second), "应为 98 前缀 17 位纯数字,实际: " + second);
|
|
|
- assertNotEquals(first, second, "同毫秒连续生成应由序列位区分,不得同号");
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void createRegeneratesOrderNoWhenUniqueKeyConflicts() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(), any(), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
- when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone(anyString())).thenReturn(List.of());
|
|
|
- org.springframework.dao.DataIntegrityViolationException orderNoConflict =
|
|
|
- new org.springframework.dao.DataIntegrityViolationException("insert failed",
|
|
|
- new java.sql.SQLException(
|
|
|
- "Duplicate entry '98001' for key 'flash_delivery_order.uk_flash_order_no'"));
|
|
|
- // 捕获器拿到的是同一对象引用,重试改单号后两次取值相同;改为每次调用时记录当时单号
|
|
|
- java.util.List<String> attempted = new java.util.ArrayList<>();
|
|
|
- doAnswer(invocation -> {
|
|
|
- FlashDeliveryOrder order = invocation.getArgument(0);
|
|
|
- attempted.add(order.getOrderNo());
|
|
|
- if (attempted.size() == 1) throw orderNoConflict;
|
|
|
- order.setId(99L);
|
|
|
- return 1;
|
|
|
- }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
|
|
|
-
|
|
|
- fixture.service.create(7L, createRequest());
|
|
|
-
|
|
|
- verify(fixture.orderMapper, times(2)).insert(any(FlashDeliveryOrder.class));
|
|
|
- assertEquals(2, attempted.size());
|
|
|
- assertNotEquals(attempted.get(0), attempted.get(1), "撞号重试后应换新单号");
|
|
|
- assertTrue(isFlashOrderNo(attempted.get(1)), "重试后仍应为新格式单号");
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void createKeepsIdempotencyFallbackForNonOrderNoConflicts() {
|
|
|
- Fixture fixture = new Fixture();
|
|
|
- when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
- when(fixture.routeService.calculate(any(), any(), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
- when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone(anyString())).thenReturn(List.of());
|
|
|
- org.springframework.dao.DataIntegrityViolationException idempotentConflict =
|
|
|
- new org.springframework.dao.DataIntegrityViolationException("insert failed",
|
|
|
- new java.sql.SQLException(
|
|
|
- "Duplicate entry '7-req-1' for key 'flash_delivery_order.uk_user_request'"));
|
|
|
- doThrow(idempotentConflict).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
|
|
|
- // 前置幂等查询放行(null);插入冲突后 catch 内按 v1 实现走 FOR UPDATE 复查 + lockOrder 锁单
|
|
|
- FlashDeliveryOrder concurrent = new FlashDeliveryOrder();
|
|
|
- concurrent.setId(55L);
|
|
|
- concurrent.setOrderNo("981789000000011");
|
|
|
- when(fixture.orderMapper.selectByUserRequestId(eq(7L), anyString())).thenReturn(null);
|
|
|
- when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(concurrent);
|
|
|
- when(fixture.orderMapper.selectById(55L)).thenReturn(concurrent);
|
|
|
-
|
|
|
- var detail = fixture.service.create(7L, createRequest());
|
|
|
-
|
|
|
- assertEquals(55L, detail.getId());
|
|
|
- verify(fixture.orderMapper, times(1)).insert(any(FlashDeliveryOrder.class));
|
|
|
- }
|
|
|
-
|
|
|
- /** 闪送新格式单号:98 开头、17 位纯数字。 */
|
|
|
- private static boolean isFlashOrderNo(String orderNo) {
|
|
|
- return orderNo != null && orderNo.startsWith("98") && orderNo.length() == 17
|
|
|
- && orderNo.chars().allMatch(Character::isDigit);
|
|
|
- }
|
|
|
-
|
|
|
- private static class Fixture {
|
|
|
- final com.ruoyi.system.mapper.flash.FlashDeliveryOrderStopMapper stopMapper = mock(com.ruoyi.system.mapper.flash.FlashDeliveryOrderStopMapper.class);
|
|
|
- final com.ruoyi.system.mapper.flash.FlashDeliveryTargetAdjustmentMapper adjustmentMapper = mock(com.ruoyi.system.mapper.flash.FlashDeliveryTargetAdjustmentMapper.class);
|
|
|
- final FlashDeliveryOrderMapper orderMapper = mock(FlashDeliveryOrderMapper.class);
|
|
|
- final FlashDeliveryPricingMapper pricingMapper = mock(FlashDeliveryPricingMapper.class);
|
|
|
- final FlashDeliveryOrderImageMapper imageMapper = mock(FlashDeliveryOrderImageMapper.class);
|
|
|
- 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 RiderDeliveryAcceptanceService acceptanceService = mock(RiderDeliveryAcceptanceService.class);
|
|
|
- final com.ruoyi.system.service.PaymentMethodGateService paymentMethodGateService =
|
|
|
- mock(com.ruoyi.system.service.PaymentMethodGateService.class);
|
|
|
- final FlashDeliveryNotificationService notificationService = mock(FlashDeliveryNotificationService.class);
|
|
|
- final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(
|
|
|
- orderMapper, pricingMapper, imageMapper, logMapper, userMapper,
|
|
|
- routeService, new FlashDeliveryPricingCalculator(), lockService, acceptanceService, stopMapper, adjustmentMapper,
|
|
|
- paymentMethodGateService, notificationService);
|
|
|
-
|
|
|
- Fixture() {
|
|
|
- when(paymentMethodGateService.listAvailable(any(), any(), any())).thenReturn(List.of(
|
|
|
- new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("COD", "1", true),
|
|
|
- new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("LINE_PAY", "3", true),
|
|
|
- new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("OFFLINE_TRANSFER", "6", true)));
|
|
|
- // Existing single-stop scenarios now explicitly provide station rows; production never synthesizes legacy stops.
|
|
|
- when(orderMapper.selectByIdForUpdate(any())).thenAnswer(i -> {
|
|
|
- FlashDeliveryOrder current = orderMapper.selectById((Long) i.getArgument(0));
|
|
|
- if (current != null && current.getPickupStopCount() == null) current.setPickupStopCount(1);
|
|
|
- return current;
|
|
|
- });
|
|
|
- when(stopMapper.insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderStop.class))).thenReturn(1);
|
|
|
- when(stopMapper.updateById(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderStop.class))).thenReturn(1);
|
|
|
- when(imageMapper.insert(any(FlashDeliveryOrderImage.class))).thenReturn(1);
|
|
|
- when(stopMapper.selectList(any())).thenAnswer(i -> {
|
|
|
- FlashDeliveryOrder current = orderMapper.selectById(10L);
|
|
|
- var pickup = new com.ruoyi.system.domain.flash.FlashDeliveryOrderStop();
|
|
|
- pickup.setId(101L); pickup.setOrderId(10L); pickup.setStopType("PICKUP"); pickup.setStopOrder(0);
|
|
|
- pickup.setName("pickup contact"); pickup.setPhone("0911111111"); pickup.setAddress("pickup road");
|
|
|
- pickup.setLatitude(new BigDecimal("25.0100")); pickup.setLongitude(new BigDecimal("121.5100"));
|
|
|
- pickup.setPackageType("DOCUMENT"); pickup.setQuantity(1); pickup.setWeightRange("UP_TO_5_KG");
|
|
|
- pickup.setStatus(current != null && List.of("PICKED_UP", "DELIVERED", "COMPLETED").contains(current.getStatus())
|
|
|
- ? "PICKED_UP" : "PENDING");
|
|
|
- var delivery = new com.ruoyi.system.domain.flash.FlashDeliveryOrderStop();
|
|
|
- delivery.setId(102L); delivery.setOrderId(10L); delivery.setStopType("DELIVERY"); delivery.setStopOrder(1); delivery.setStatus("PENDING");
|
|
|
- return List.of(pickup, delivery);
|
|
|
- });
|
|
|
- when(lockService.withLock(any(), any())).thenAnswer(invocation -> {
|
|
|
- RiderDeliveryLockService.LockedCall<?> call = invocation.getArgument(1);
|
|
|
- return call.call();
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+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.FlashDeliveryReasonRequest;
|
|
|
+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.GeoPoint;
|
|
|
+import com.ruoyi.app.flashdelivery.route.RouteDistance;
|
|
|
+import com.ruoyi.app.order.RiderDeliveryAcceptanceService;
|
|
|
+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;
|
|
|
+import com.ruoyi.system.domain.flash.FlashDeliveryPricing;
|
|
|
+import com.ruoyi.system.mapper.InfoUserMapper;
|
|
|
+import com.ruoyi.system.mapper.flash.FlashDeliveryOrderImageMapper;
|
|
|
+import com.ruoyi.system.mapper.flash.FlashDeliveryOrderLogMapper;
|
|
|
+import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
|
|
|
+import com.ruoyi.system.mapper.flash.FlashDeliveryPricingMapper;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.BeforeAll;
|
|
|
+import org.junit.jupiter.api.AfterAll;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.mockito.InOrder;
|
|
|
+import org.mockito.MockedStatic;
|
|
|
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
|
|
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
|
|
+import org.springframework.context.support.StaticMessageSource;
|
|
|
+import com.ruoyi.common.core.domain.entity.SysDictData;
|
|
|
+import com.ruoyi.common.utils.DictUtils;
|
|
|
+import com.ruoyi.common.utils.spring.SpringUtils;
|
|
|
+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.assertNotEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+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.anyInt;
|
|
|
+import static org.mockito.ArgumentMatchers.anyString;
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
+import static org.mockito.ArgumentMatchers.isNull;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.mockStatic;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.times;
|
|
|
+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;
|
|
|
+import static org.mockito.Mockito.verifyNoInteractions;
|
|
|
+
|
|
|
+class FlashDeliveryApplicationServiceTest {
|
|
|
+ private static ConfigurableListableBeanFactory originalBeanFactory;
|
|
|
+
|
|
|
+ @BeforeAll
|
|
|
+ static void initializeMessages() {
|
|
|
+ originalBeanFactory = (ConfigurableListableBeanFactory)
|
|
|
+ ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
|
|
|
+ DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
|
|
+ StaticMessageSource messageSource = new StaticMessageSource();
|
|
|
+ messageSource.setUseCodeAsDefaultMessage(true);
|
|
|
+ beanFactory.registerSingleton("messageSource", messageSource);
|
|
|
+ new SpringUtils().postProcessBeanFactory(beanFactory);
|
|
|
+ }
|
|
|
+
|
|
|
+ @AfterAll
|
|
|
+ static void restoreMessages() {
|
|
|
+ new SpringUtils().postProcessBeanFactory(originalBeanFactory);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void quoteUsesServerRouteAndCurrentTimePricing() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3001, 600, "ROUTE"));
|
|
|
+
|
|
|
+ var quote = fixture.service.quote(request());
|
|
|
+
|
|
|
+ assertEquals(3001, quote.getDistanceMeters());
|
|
|
+ assertEquals(90L, quote.getAmount());
|
|
|
+ assertEquals(1L, quote.getPricingId());
|
|
|
+ assertEquals("00:00", quote.getStartTime());
|
|
|
+ assertEquals("24:00", quote.getEndTime());
|
|
|
+ assertEquals("ROUTE", quote.getDistanceSource());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void quoteRejectsRoutesLongerThanFortyKilometres() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(40001, 1800, "ROUTE"));
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void wrongDeliveryPinCannotPersistProofOrChangeState() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setUserType("2");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
+ order.setId(10L);
|
|
|
+ order.setRiderId(8L);
|
|
|
+ order.setStatus("PICKED_UP");
|
|
|
+ order.setPinRequired(true);
|
|
|
+ order.setDeliveryPinCode("4821");
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
|
|
|
+ request.setPinCode("1234");
|
|
|
+ request.setImageUrls(List.of("https://example.com/delivery.jpg"));
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.deliver(8L, 10L, request));
|
|
|
+ verify(fixture.imageMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class));
|
|
|
+ verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void correctDeliveryPinPersistsDeliveryProofAndChangesState() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setUserType("2");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
+ order.setId(10L);
|
|
|
+ order.setRiderId(8L);
|
|
|
+ order.setStatus("PICKED_UP");
|
|
|
+ order.setPinRequired(true);
|
|
|
+ order.setDeliveryPinCode("4821");
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("PICKED_UP"),
|
|
|
+ eq("DELIVERED"), eq("delivered_at"), any())).thenReturn(1);
|
|
|
+ FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
|
|
|
+ request.setPinCode("4821");
|
|
|
+ request.setImageUrls(List.of("https://example.com/delivery.jpg"));
|
|
|
+
|
|
|
+ fixture.service.deliver(8L, 10L, request);
|
|
|
+
|
|
|
+ ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
|
|
|
+ ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
|
|
|
+ verify(fixture.imageMapper).insert(image.capture());
|
|
|
+ assertEquals("DELIVERY", image.getValue().getProofType());
|
|
|
+ assertEquals("RIDER", image.getValue().getOperatorType());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void scheduledCreateGeneratesUserOnlyPinAndStoresRelativeSenderProof() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
|
|
|
+ .thenReturn(List.of(88L));
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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.setSenderImageUrls(List.of("/profile/upload/2026/09/07/sender.jpg"));
|
|
|
+
|
|
|
+ var result = fixture.service.create(7L, request);
|
|
|
+
|
|
|
+ assertEquals(4, result.getDeliveryPinCode().length());
|
|
|
+ ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
|
|
|
+ ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
|
|
|
+ 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());
|
|
|
+ assertEquals(new BigDecimal("3.00"), saved.getValue().getStartingDistance());
|
|
|
+ assertEquals(90L, saved.getValue().getStartingFare());
|
|
|
+ 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
|
|
|
+ void confirmPaymentRequiresDeliveredState() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 9L);
|
|
|
+ FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
|
|
|
+ pickedUp.setRiderId(9L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.confirmPayment(9L, 10L));
|
|
|
+ verify(fixture.orderMapper, never()).update(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void confirmPaymentMarksPaidAndWritesLog() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 9L);
|
|
|
+ FlashDeliveryOrder delivered = order(10L, "DELIVERED");
|
|
|
+ delivered.setRiderId(9L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(delivered);
|
|
|
+ when(fixture.orderMapper.update(any(), any())).thenReturn(1);
|
|
|
+ when(fixture.logMapper.insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class))).thenReturn(1);
|
|
|
+
|
|
|
+ fixture.service.confirmPayment(9L, 10L);
|
|
|
+
|
|
|
+ verify(fixture.orderMapper).update(any(), any());
|
|
|
+ ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderLog> logCaptor =
|
|
|
+ ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class);
|
|
|
+ verify(fixture.logMapper).insert(logCaptor.capture());
|
|
|
+ assertEquals("PAYMENT_CONFIRMED", logCaptor.getValue().getReason());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void confirmPaymentIsIdempotentOnceConfirmed() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 9L);
|
|
|
+ FlashDeliveryOrder confirmed = order(10L, "DELIVERED");
|
|
|
+ confirmed.setRiderId(9L);
|
|
|
+ confirmed.setPaymentStatus(1);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(confirmed);
|
|
|
+
|
|
|
+ fixture.service.confirmPayment(9L, 10L);
|
|
|
+
|
|
|
+ verify(fixture.orderMapper, never()).update(any(), any());
|
|
|
+ verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ 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, null);
|
|
|
+
|
|
|
+ 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());
|
|
|
+ String sql = query.getValue().getSqlSegment().toLowerCase();
|
|
|
+ var parameters = query.getValue().getParamNameValuePairs().values();
|
|
|
+ assertTrue(sql.contains("user_id"));
|
|
|
+ assertFalse(sql.contains("status"));
|
|
|
+ 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(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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),
|
|
|
+ any(Wrapper.class))).thenReturn(source);
|
|
|
+ when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of(168L));
|
|
|
+
|
|
|
+ SysDictData distanceLimit = new SysDictData();
|
|
|
+ distanceLimit.setDictValue("5");
|
|
|
+ FlashDeliveryRiderOrderPageView result;
|
|
|
+ try (MockedStatic<DictUtils> dictionary = mockStatic(DictUtils.class)) {
|
|
|
+ dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance"))
|
|
|
+ .thenReturn(List.of(distanceLimit));
|
|
|
+ result = fixture.service.riderOrders(8L, 1, 10, "newTask",
|
|
|
+ new BigDecimal("121.5000"), new BigDecimal("25.0000"));
|
|
|
+ }
|
|
|
+
|
|
|
+ assertEquals(1L, result.getNearbyTaskCount());
|
|
|
+ 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 =
|
|
|
+ ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
|
|
|
+ verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
|
|
|
+ String sql = query.getValue().getSqlSegment().toLowerCase();
|
|
|
+ var parameters = query.getValue().getParamNameValuePairs().values();
|
|
|
+ assertTrue(sql.contains("status"));
|
|
|
+ assertTrue(sql.contains("rider_id is null"));
|
|
|
+ assertTrue(sql.contains("is_display"));
|
|
|
+ assertTrue(sql.contains("vehicle_type"));
|
|
|
+ assertTrue(sql.contains("scheduled_pickup_start_at"));
|
|
|
+ assertTrue(sql.contains("scheduled_pickup_end_at"));
|
|
|
+ assertTrue(sql.contains("st_distance_sphere"));
|
|
|
+ assertTrue(parameters.contains("WAITING_ACCEPTANCE"));
|
|
|
+ assertTrue(parameters.contains("NOW"));
|
|
|
+ assertTrue(parameters.stream().anyMatch(Date.class::isInstance));
|
|
|
+
|
|
|
+ ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> summaryQuery =
|
|
|
+ ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
|
|
|
+ verify(fixture.orderMapper).selectObjs(summaryQuery.capture());
|
|
|
+ String summarySql = summaryQuery.getValue().getSqlSegment().toLowerCase();
|
|
|
+ var summaryParameters = summaryQuery.getValue().getParamNameValuePairs().values();
|
|
|
+ assertTrue(summarySql.contains("rider_id is null"));
|
|
|
+ assertTrue(summarySql.contains("scheduled_pickup_start_at"));
|
|
|
+ assertTrue(summarySql.contains("scheduled_pickup_end_at"));
|
|
|
+ assertTrue(summarySql.contains("st_distance_sphere"));
|
|
|
+ assertTrue(summaryParameters.contains("WAITING_ACCEPTANCE"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ void riderNewTaskWithoutPositionFallsBackToNewestFirst() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
+ any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
+ when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of());
|
|
|
+
|
|
|
+ fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
|
|
|
+
|
|
|
+ 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());
|
|
|
+ String sql = query.getValue().getSqlSegment().toLowerCase();
|
|
|
+ assertTrue(sql.contains("order by create_time desc"));
|
|
|
+ assertFalse(sql.contains("st_distance_sphere"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ void riderTabsMapStatusesBeforePagingAndRestrictAssignedTabsToRider() {
|
|
|
+ assertRiderTab("toPickup", List.of("ACCEPTED"));
|
|
|
+ assertRiderTab("delivering", List.of("PICKED_UP"));
|
|
|
+ assertRiderTab("completed", List.of("DELIVERED", "COMPLETED"));
|
|
|
+ assertRiderTab("cancelled", List.of("CANCELLED"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void riderListRejectsUnknownTabAndUnpairedOrInvalidCoordinates() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.riderOrders(8L, 1, 10, "refund", null, null));
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.riderOrders(8L, 1, 10, null, null, null));
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.riderOrders(8L, 1, 10, "newTask", BigDecimal.ONE, null));
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.riderOrders(8L, 1, 10, "newTask",
|
|
|
+ new BigDecimal("181"), BigDecimal.ZERO));
|
|
|
+ verify(fixture.orderMapper, never()).selectPage(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void flashNewTaskListAndAcceptRequireFlashDeliveryType() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setUserType("2");
|
|
|
+ rider.setDeliveryType("FOOD");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.riderOrders(8L, 1, 10, "newTask", null, null));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
+ verify(fixture.orderMapper, never()).selectPage(any(), any());
|
|
|
+ verify(fixture.orderMapper, never()).accept(any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void riderDetailHidesWaitingFlashOrdersFromNonFlashRiders() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setUserType("2");
|
|
|
+ rider.setDeliveryType("FOOD");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.riderDetail(8L, 10L));
|
|
|
+ verify(fixture.imageMapper, never()).selectList(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ void enabledFlashRiderStillListsNewTasks() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setUserType("2");
|
|
|
+ rider.setDeliveryType("FLASH");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
+ any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
+
|
|
|
+ fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
|
|
|
+ verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
+ any(Wrapper.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ void ownOrderTabsRemainAccessibleRegardlessOfDeliveryType() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setUserType("2");
|
|
|
+ rider.setDeliveryType("FOOD");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
+ any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
+
|
|
|
+ fixture.service.riderOrders(8L, 1, 10, "toPickup", null, null);
|
|
|
+ verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
+ any(Wrapper.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void assignedRiderTabsIgnoreLocationParameters() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
+ any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
+
|
|
|
+ fixture.service.riderOrders(8L, 1, 10, "toPickup",
|
|
|
+ new BigDecimal("181"), new BigDecimal("91"));
|
|
|
+
|
|
|
+ verify(fixture.orderMapper).selectPage(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void preAcceptRiderDetailIsDirectAndShowsTextAddressWithoutSensitiveFields() {
|
|
|
+ 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);
|
|
|
+
|
|
|
+ assertEquals(10L, result.getId());
|
|
|
+ assertEquals("pickup road", result.getPickup().getAddress());
|
|
|
+ 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());
|
|
|
+ assertNull(result.getPickup().getLatitude());
|
|
|
+ assertNull(result.getDelivery().getName());
|
|
|
+ assertNull(result.getDelivery().getPhone());
|
|
|
+ assertNull(result.getDelivery().getLongitude());
|
|
|
+ assertNull(result.getDelivery().getLatitude());
|
|
|
+ assertNull(result.getDeliveryPinCode());
|
|
|
+ assertNull(result.getUserNote());
|
|
|
+ assertNull(result.getUser());
|
|
|
+ assertTrue(result.getSenderImageUrls().isEmpty());
|
|
|
+ assertTrue(result.getPickupImageUrls().isEmpty());
|
|
|
+ assertTrue(result.getDeliveryImageUrls().isEmpty());
|
|
|
+ verify(fixture.userMapper, never()).selectInfoUserByUserId(7L);
|
|
|
+ verify(fixture.imageMapper, never()).selectList(any());
|
|
|
+ verify(fixture.logMapper, never()).selectList(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userDetailReturnsDirectOrderAndGroupsImageUrlsWithoutRawLogs() {
|
|
|
+ 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"),
|
|
|
+ image("PICKUP", "https://example.com/pickup.jpg"),
|
|
|
+ image("DELIVERY", "https://example.com/delivery.jpg")));
|
|
|
+
|
|
|
+ var result = fixture.service.userDetail(7L, 10L);
|
|
|
+
|
|
|
+ assertEquals(10L, result.getId());
|
|
|
+ assertEquals(List.of("https://example.com/sender.jpg"), result.getSenderImageUrls());
|
|
|
+ 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 userDetailReturnsRiderInfoAfterAcceptance() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, ACCEPTED);
|
|
|
+ order.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setUserType("2");
|
|
|
+ rider.setNickName("闪送骑士");
|
|
|
+ rider.setTelPhone("0933333333");
|
|
|
+ rider.setVehicleType("2");
|
|
|
+ rider.setLicensePlate("ABC-1234");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+
|
|
|
+ var result = fixture.service.userDetail(7L, 10L);
|
|
|
+
|
|
|
+ assertEquals(8L, result.getRider().getUserId());
|
|
|
+ assertEquals("闪送骑士", result.getRider().getNickName());
|
|
|
+ assertEquals("0933333333", result.getRider().getTelPhone());
|
|
|
+ assertEquals("ABC-1234", result.getRider().getLicensePlate());
|
|
|
+ assertNull(result.getUser());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void assignedRiderDetailReturnsSenderInfo() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ FlashDeliveryOrder order = order(10L, ACCEPTED);
|
|
|
+ order.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ InfoUser sender = new InfoUser();
|
|
|
+ sender.setUserId(7L);
|
|
|
+ sender.setNickName("寄件达人");
|
|
|
+ sender.setPhone("0911111111");
|
|
|
+ sender.setAvatar("https://example.com/avatar.png");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(7L)).thenReturn(sender);
|
|
|
+
|
|
|
+ var result = fixture.service.riderDetail(8L, 10L);
|
|
|
+
|
|
|
+ assertEquals(7L, result.getUser().getUserId());
|
|
|
+ assertEquals("寄件达人", result.getUser().getNickName());
|
|
|
+ assertEquals("0911111111", result.getUser().getPhone());
|
|
|
+ assertEquals("https://example.com/avatar.png", result.getUser().getAvatar());
|
|
|
+ assertNull(result.getRider());
|
|
|
+ }
|
|
|
+
|
|
|
+ @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 senderRatesRiderOnCompletedOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ when(fixture.orderMapper.rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class))).thenReturn(1);
|
|
|
+
|
|
|
+ fixture.service.rateRider(7L, 10L, rating(4.5));
|
|
|
+
|
|
|
+ verify(fixture.orderMapper).rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void rateRiderRejectsUserOtherThanSender() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.rateRider(88L, 10L, rating(4.5)));
|
|
|
+ verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void rateRiderRejectsOrderNotCompletedOrWithoutRider() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder delivered = order(10L, "DELIVERED");
|
|
|
+ delivered.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(delivered);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(4.5)));
|
|
|
+
|
|
|
+ FlashDeliveryOrder completedWithoutRider = order(11L, "COMPLETED");
|
|
|
+ when(fixture.orderMapper.selectById(11L)).thenReturn(completedWithoutRider);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 11L, rating(4.5)));
|
|
|
+
|
|
|
+ verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void rateRiderRejectsAlreadyRatedOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ order.setRiderStars(4.5);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(3.0)));
|
|
|
+ verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void rateRiderRejectsConcurrentDuplicateSubmission() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ when(fixture.orderMapper.rateRider(eq(10L), eq(7L), eq(4.5), any(Date.class))).thenReturn(0);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(4.5)));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void rateRiderRejectsMissingOrOutOfRangeStars() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, null));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(null)));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(0.5)));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.rateRider(7L, 10L, rating(5.5)));
|
|
|
+
|
|
|
+ verify(fixture.orderMapper, never()).selectById(any());
|
|
|
+ verify(fixture.orderMapper, never()).rateRider(any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userDetailReturnsRiderActiveCardForDeliveredOfflineOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "DELIVERED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ order.setPayType("6");
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ rider(fixture, 8L);
|
|
|
+ com.ruoyi.system.domain.InfoBankCard card = new com.ruoyi.system.domain.InfoBankCard();
|
|
|
+ card.setBankName("中國信託");
|
|
|
+ card.setAccountNo("1234567890");
|
|
|
+ card.setAccountName("王大明");
|
|
|
+ when(fixture.bankCardMapper.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(card));
|
|
|
+
var result = fixture.service.userDetail(7L, 10L);
|
|
|
+
assertNotNull(result.getRiderBankCard());
|
|
|
+ assertEquals("中國信託", result.getRiderBankCard().getBankName());
|
|
|
+ assertEquals("1234567890", result.getRiderBankCard().getAccountNo());
|
|
|
+ assertEquals("王大明", result.getRiderBankCard().getAccountName());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userDetailHidesRiderCardBeforeDeliveryOrForNonOfflineOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ // 到付单:已送达也不返回卡
|
|
|
+ FlashDeliveryOrder cod = order(10L, "DELIVERED");
|
|
|
+ cod.setRiderId(8L);
|
|
|
+ cod.setPayType("1");
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(cod);
|
|
|
+ assertNull(fixture.service.userDetail(7L, 10L).getRiderBankCard());
|
|
|
+ // 线下转账单:未送达不返回卡
|
|
|
+ FlashDeliveryOrder notDelivered = order(10L, "PICKED_UP");
|
|
|
+ notDelivered.setRiderId(8L);
|
|
|
+ notDelivered.setPayType("6");
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(notDelivered);
|
|
|
+ assertNull(fixture.service.userDetail(7L, 10L).getRiderBankCard());
|
|
|
+ // 条件不满足时连卡查询都不发起
|
|
|
+ verify(fixture.bankCardMapper, never()).selectList(any(Wrapper.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userDetailHidesRiderCardWhenRiderHasNoActiveCard() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "DELIVERED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ order.setPayType("6");
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ rider(fixture, 8L);
|
|
|
+ when(fixture.bankCardMapper.selectList(any(Wrapper.class))).thenReturn(java.util.List.of());
|
|
|
+
assertNull(fixture.service.userDetail(7L, 10L).getRiderBankCard());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ void riderPoolPassesRiderIdentityAndExcludesNotReadyOptions() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ // 闸门按骑手身份判定就绪度:无启用卡时线下转账 ready=false
|
|
|
+ when(fixture.paymentMethodGateService.listAvailable(
|
|
|
+ eq(com.ruoyi.system.service.PaymentMethodGateService.GateScope.RIDER_FLASH), isNull(), eq(8L)))
|
|
|
+ .thenReturn(java.util.List.of(
|
|
|
+ new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("COD", "1", true),
|
|
|
+ new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("OFFLINE_TRANSFER", "6", false)));
|
|
|
+ when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), any(Wrapper.class)))
|
|
|
+ .thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
+ when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(java.util.List.of(168L));
|
|
|
+ SysDictData distanceLimit = new SysDictData();
|
|
|
+ distanceLimit.setDictValue("5");
|
|
|
+ try (MockedStatic<DictUtils> dictionary = mockStatic(DictUtils.class)) {
|
|
|
+ dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance"))
|
|
|
+ .thenReturn(java.util.List.of(distanceLimit));
|
|
|
+ fixture.service.riderOrders(8L, 1, 10, "newTask", new BigDecimal("121.5000"), new BigDecimal("25.0000"));
|
|
|
+ }
|
|
|
+ // 抢单过滤必须把骑手身份传给闸门(就绪度=骑手启用卡由闸门判定并单测覆盖)
|
|
|
+ verify(fixture.paymentMethodGateService).listAvailable(
|
|
|
+ eq(com.ruoyi.system.service.PaymentMethodGateService.GateScope.RIDER_FLASH), isNull(), eq(8L));
|
|
|
+ // 就绪度过滤后仍生成 pay_type IN 条件(OFFLINE ready=false 不进集合)
|
|
|
+ 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());
|
|
|
+ String sql = query.getValue().getSqlSegment();
|
|
|
+ assertTrue(sql.contains("pay_type IN"));
|
|
|
+ // 两个 IN 占位符:现金4 + 到付1(6 被 ready=false 过滤掉)
|
|
|
+ String segment = sql.substring(sql.indexOf("pay_type IN"));
|
|
|
+ String inside = segment.substring(0, segment.indexOf(')'));
|
|
|
+ assertEquals(2, inside.split(java.util.regex.Pattern.quote("#{")).length - 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userDetailReturnsRiderStars() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ order.setRiderStars(4.5);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
+
|
|
|
+ var view = fixture.service.userDetail(7L, 10L);
|
|
|
+
|
|
|
+ assertEquals(4.5, view.getRiderStars());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userDetailRiderCarriesFlashAverageStars() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "ACCEPTED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setNickName("闪送骑手");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ when(fixture.orderMapper.selectRiderAverageStars(8L)).thenReturn(4.8);
|
|
|
+ when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
+
|
|
|
+ var view = fixture.service.userDetail(7L, 10L);
|
|
|
+
|
|
|
+ assertEquals(4.8, view.getRider().getStar());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userDetailRiderFallsBackToDefaultStarsWithoutRatings() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "ACCEPTED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ when(fixture.orderMapper.selectRiderAverageStars(8L)).thenReturn(null);
|
|
|
+ when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
+
|
|
|
+ var view = fixture.service.userDetail(7L, 10L);
|
|
|
+
|
|
|
+ assertEquals(4.5, view.getRider().getStar());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userDetailByOrderNoReturnsParticipantDetail() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ order.setRiderStars(4.5);
|
|
|
+ when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
+
|
|
|
+ var view = fixture.service.userDetailByOrderNo(7L, "FL-20260813-021");
|
|
|
+
|
|
|
+ assertEquals(10L, view.getId());
|
|
|
+ assertEquals(4.5, view.getRiderStars());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userDetailByOrderNoRejectsNonParticipantAndUnknownOrderNo() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = order(10L, "COMPLETED");
|
|
|
+ order.setReceiverUserId(null);
|
|
|
+ when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.userDetailByOrderNo(88L, "FL-20260813-021"));
|
|
|
+
|
|
|
+ when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(null);
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.userDetailByOrderNo(7L, "FL-UNKNOWN"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void riderDetailByOrderNoReturnsAssignedOrderDetail() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ FlashDeliveryOrder order = order(10L, "ACCEPTED");
|
|
|
+ order.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(order);
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(7L)).thenReturn(new InfoUser());
|
|
|
+ when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
|
|
+
|
|
|
+ var view = fixture.service.riderDetailByOrderNo(8L, "FL-20260813-021");
|
|
|
+
|
|
|
+ assertEquals(10L, view.getId());
|
|
|
+ assertNotNull(view.getUser());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void riderDetailByOrderNoRejectsUnknownOrderNo() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(null);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.riderDetailByOrderNo(8L, "FL-UNKNOWN"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @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();
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setUserType("2");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
|
|
|
+ request.setImageUrls(List.of());
|
|
|
+
|
|
|
+ assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
|
|
|
+ verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void atomicAcceptFailureDoesNotBindSecondRider() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ 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));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 预约单已过取件结束时间:即使骑手绕过列表直接持有订单 ID 请求接单,锁内校验也必须拒绝。
|
|
|
+ @Test
|
|
|
+ void expiredScheduledOrderCannotBeAcceptedEvenWhenItsIdIsKnown() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
|
|
|
+ Date now = new Date();
|
|
|
+ order.setDeliveryMode("SCHEDULED");
|
|
|
+ order.setScheduledPickupStartAt(new Date(now.getTime() - 31L * 60L * 1000L));
|
|
|
+ order.setScheduledPickupEndAt(new Date(now.getTime() - 60L * 1000L));
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
+ verify(fixture.orderMapper, never()).accept(any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void flashAcceptUsesRiderLockAndChecksAcceptancePolicyBeforeAtomicUpdate() {
|
|
|
+ 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.acceptanceService, fixture.orderMapper);
|
|
|
+ order.verify(fixture.lockService).withLock(eq(8L), any(RiderDeliveryLockService.LockedCall.class));
|
|
|
+ order.verify(fixture.acceptanceService).assertCanAcceptFlash(8L, "NORMAL");
|
|
|
+ order.verify(fixture.orderMapper).accept(eq(10L), eq(8L), any(Date.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void flashAcceptanceConflictDoesNotUpdateOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
|
|
|
+ doThrow(new ServiceException("exclusive"))
|
|
|
+ .when(fixture.acceptanceService).assertCanAcceptFlash(8L, "NORMAL");
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
+
|
|
|
+ verify(fixture.orderMapper, never()).accept(any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void acceptSuccessNotifiesSenderOfOrder() {
|
|
|
+ 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);
|
|
|
+
|
|
|
+ verify(fixture.notificationService).notifyAccepted(waiting);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void acceptFailureDoesNotTriggerNotification() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
|
|
|
+ when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(0);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
|
|
|
+
|
|
|
+ verify(fixture.notificationService, never()).notifyAccepted(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void pickupSuccessNotifiesSenderOfOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ FlashDeliveryOrder accepted = order(10L, ACCEPTED);
|
|
|
+ accepted.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
|
|
|
+ // 逐站取件走 updateFulfilment 条件更新而非 transitionByRider。
|
|
|
+ when(fixture.orderMapper.updateFulfilment(any())).thenReturn(1);
|
|
|
+ FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
|
|
|
+ request.setImageUrls(List.of("https://example.com/pickup.jpg"));
|
|
|
+
|
|
|
+ fixture.service.pickup(8L, 10L, request);
|
|
|
+
|
|
|
+ verify(fixture.notificationService).notifyPickedUp(accepted);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void deliverSuccessNotifiesSenderOfOrder() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
|
|
|
+ pickedUp.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
|
|
|
+ when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("PICKED_UP"),
|
|
|
+ eq("DELIVERED"), eq("delivered_at"), any())).thenReturn(1);
|
|
|
+ FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
|
|
|
+ request.setPinCode("4821");
|
|
|
+ request.setImageUrls(List.of("https://example.com/delivery.jpg"));
|
|
|
+
|
|
|
+ fixture.service.deliver(8L, 10L, request);
|
|
|
+
|
|
|
+ verify(fixture.notificationService).notifyDelivered(pickedUp);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void proofTransitionFailureDoesNotTriggerNotification() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ FlashDeliveryOrder accepted = order(10L, ACCEPTED);
|
|
|
+ accepted.setRiderId(8L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
|
|
|
+ when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("ACCEPTED"),
|
|
|
+ eq("PICKED_UP"), eq("picked_up_at"), any())).thenReturn(0);
|
|
|
+ FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
|
|
|
+ request.setImageUrls(List.of("https://example.com/pickup.jpg"));
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.pickup(8L, 10L, request));
|
|
|
+
|
|
|
+ verify(fixture.notificationService, never()).notifyPickedUp(any());
|
|
|
+ verify(fixture.notificationService, never()).notifyDelivered(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void userCancelSuccessNotifiesAssignedRider() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder accepted = order(10L, ACCEPTED);
|
|
|
+ accepted.setRiderId(9L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
|
|
|
+ when(fixture.orderMapper.cancel(eq(10L), eq("ACCEPTED"), eq("USER"), eq(7L),
|
|
|
+ anyString(), any(Date.class))).thenReturn(1);
|
|
|
+
|
|
|
+ fixture.service.userCancel(7L, 10L, reason("改行程"));
|
|
|
+
|
|
|
+ verify(fixture.notificationService).notifyCancelled(accepted);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void adminCancelSuccessNotifiesAssignedRider() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder pickedUp = order(10L, "PICKED_UP");
|
|
|
+ pickedUp.setRiderId(9L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(pickedUp);
|
|
|
+ // 平台异常取消先写介入审计,再走条件取消。
|
|
|
+ when(fixture.orderMapper.update(any(), any())).thenReturn(1);
|
|
|
+ when(fixture.orderMapper.cancel(eq(10L), eq("PICKED_UP"), eq("ADMIN"), eq(1L),
|
|
|
+ anyString(), any(Date.class))).thenReturn(1);
|
|
|
+ FlashDeliveryReasonRequest request = reason("异常收口");
|
|
|
+ request.setProblemStopId(101L);
|
|
|
+ request.setHandlingResult("平台介入收口");
|
|
|
+ // 已取货订单必须记录物品去向和交接凭证。
|
|
|
+ request.setPickedGoodsDisposition("已取物品交回寄件人");
|
|
|
+ request.setHandoffImageUrls(List.of("https://example.com/handoff.jpg"));
|
|
|
+
|
|
|
+ fixture.service.adminCancel(1L, 10L, request);
|
|
|
+
|
|
|
+ verify(fixture.notificationService).notifyCancelled(pickedUp);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void cancelFailureDoesNotTriggerNotification() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder accepted = order(10L, ACCEPTED);
|
|
|
+ accepted.setRiderId(9L);
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(accepted);
|
|
|
+ when(fixture.orderMapper.cancel(eq(10L), eq("ACCEPTED"), eq("USER"), eq(7L),
|
|
|
+ anyString(), any(Date.class))).thenReturn(0);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.userCancel(7L, 10L, reason("改行程")));
|
|
|
+
|
|
|
+ verify(fixture.notificationService, never()).notifyCancelled(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void excludedEventsNeverTriggerNotification() {
|
|
|
+ // 加小费、完成、过期预约自动取消三类事件不产生任何推送。
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ editableOrder(fixture);
|
|
|
+ when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
|
|
|
+ fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
|
|
|
+
|
|
|
+ // 合并后的 complete 要求订单已送达且全部取货站完成。
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "DELIVERED"));
|
|
|
+ when(fixture.orderMapper.transitionByStatus(eq(10L), eq("DELIVERED"), eq("COMPLETED"),
|
|
|
+ eq("completed_at"), any(Date.class))).thenReturn(1);
|
|
|
+ assertTrue(fixture.service.complete(1L, "ADMIN", 10L));
|
|
|
+
|
|
|
+ when(fixture.orderMapper.cancelExpiredScheduled(eq(10L), any(Date.class))).thenReturn(1);
|
|
|
+ assertTrue(fixture.service.cancelExpiredScheduledOrder(10L, new Date()));
|
|
|
+
|
|
|
+ verifyNoInteractions(fixture.notificationService);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void immediateOrderIsVisibleAndNotifiesAvailableRiders() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ prepareCreate(fixture);
|
|
|
+ FlashDeliveryCreateRequest request = createRequest();
|
|
|
+ request.setDeliveryMode("NOW");
|
|
|
+ request.setScheduledPickupStartAt(null);
|
|
|
+ request.setScheduledPickupEndAt(null);
|
|
|
+
|
|
|
+ fixture.service.create(7L, request);
|
|
|
+
|
|
|
+ ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
+ verify(fixture.orderMapper).insert(saved.capture());
|
|
|
+ assertTrue(saved.getValue().getIsDisplay());
|
|
|
+ verify(fixture.notificationService).notifyAvailable(saved.getValue());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void scheduledOrderStartsHiddenAndDoesNotNotifyBeforeWindow() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ prepareCreate(fixture);
|
|
|
+
|
|
|
+ fixture.service.create(7L, createRequest());
|
|
|
+
|
|
|
+ ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
+ verify(fixture.orderMapper).insert(saved.capture());
|
|
|
+ assertFalse(saved.getValue().getIsDisplay());
|
|
|
+ verify(fixture.notificationService, never()).notifyAvailable(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void scheduledOrderBecomesVisibleAndNotifiesOnlyAfterAtomicOpenSucceeds() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder scheduled = order(10L, "WAITING_ACCEPTANCE");
|
|
|
+ scheduled.setDeliveryMode("SCHEDULED");
|
|
|
+ scheduled.setIsDisplay(false);
|
|
|
+ Date now = new Date();
|
|
|
+ when(fixture.orderMapper.selectById(10L)).thenReturn(scheduled);
|
|
|
+ when(fixture.orderMapper.openScheduledForRiders(10L, now)).thenReturn(1);
|
|
|
+
|
|
|
+ assertTrue(fixture.service.openScheduledOrderForRiders(10L, now));
|
|
|
+
|
|
|
+ assertTrue(scheduled.getIsDisplay());
|
|
|
+ verify(fixture.notificationService).notifyAvailable(scheduled);
|
|
|
+
|
|
|
+ when(fixture.orderMapper.openScheduledForRiders(10L, now)).thenReturn(0);
|
|
|
+ assertFalse(fixture.service.openScheduledOrderForRiders(10L, now));
|
|
|
+ verify(fixture.notificationService).notifyAvailable(scheduled);
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryReasonRequest reason(String text) {
|
|
|
+ FlashDeliveryReasonRequest request = new FlashDeliveryReasonRequest();
|
|
|
+ request.setReason(text);
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void oversizedProofUrlIsRejectedBeforeStateTransition() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(8L);
|
|
|
+ rider.setUserType("2");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
|
|
|
+ FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
|
|
|
+ request.setImageUrls(List.of("https://example.com/" + "a".repeat(1000)));
|
|
|
+
|
|
|
+ assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
|
|
|
+ verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void missingServiceTypeReturnsBusinessErrorInsteadOfNullPointer() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryQuoteRequest request = request();
|
|
|
+ request.setServiceType(null);
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quote(request));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void equalTextAddressesAreRejectedEvenWhenCoordinatesDiffer() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryQuoteRequest request = request();
|
|
|
+ request.getDelivery().setAddress(request.getPickup().getAddress());
|
|
|
+ request.getDelivery().setAddressDetail(request.getPickup().getAddressDetail());
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quote(request));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void concurrentPricingUpdateIsRejected() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectById(1L)).thenReturn(pricing());
|
|
|
+ when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("12:00"), anyInt(), eq(1L))).thenReturn(0);
|
|
|
+ when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
|
|
|
+ FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
+ request.setEndTime("12:00");
|
|
|
+ request.setConfigVersion(1);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class,
|
|
|
+ () -> fixture.service.updatePricing(1L, 1L, request));
|
|
|
+ verify(fixture.pricingMapper, never()).updateById(any(FlashDeliveryPricing.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void overlappingPricingPeriodIsRejectedBeforeInsert() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
+ when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull())).thenReturn(1);
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
|
|
|
+
|
|
|
+ request.setDistance(new BigDecimal("0.004"));
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
|
|
|
+
|
|
|
+ verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void pricingDistanceHalfCentimetreNormalizesToDatabaseMinimum() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
+ request.setDistance(new BigDecimal("0.005"));
|
|
|
+
|
|
|
+ fixture.service.createPricing(1L, request);
|
|
|
+
|
|
|
+ ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
|
|
|
+ verify(fixture.pricingMapper).insert(saved.capture());
|
|
|
+ assertEquals(new BigDecimal("0.01"), saved.getValue().getDistance());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void pricingDistanceThatRoundsToZeroIsRejectedBeforePersistence() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
+ request.setDistance(new BigDecimal("0.001"));
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
|
|
|
+
|
|
|
+ verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createPricingPersistsImmediatelyActiveTimePeriod() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
+ when(fixture.pricingMapper.countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull())).thenReturn(0);
|
|
|
+
|
|
|
+ fixture.service.createPricing(7L, request);
|
|
|
+
|
|
|
+ ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
|
|
|
+ verify(fixture.pricingMapper).insert(saved.capture());
|
|
|
+ assertEquals(1, saved.getValue().getConfigVersion());
|
|
|
+ assertEquals(7L, saved.getValue().getUpdatedBy());
|
|
|
+ assertEquals(90L, saved.getValue().getStartingFare());
|
|
|
+
|
|
|
+ InOrder writeOrder = inOrder(fixture.pricingMapper);
|
|
|
+ writeOrder.verify(fixture.pricingMapper)
|
|
|
+ .countOverlapping(eq("00:00"), eq("24:00"), anyInt(), isNull());
|
|
|
+ writeOrder.verify(fixture.pricingMapper).insert(any(FlashDeliveryPricing.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void homeOmitsServiceWithoutCurrentPricingPeriod() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+
|
|
|
+ assertTrue(fixture.service.home().getServices().isEmpty());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void quoteWithoutCurrentPricingPeriodFailsBeforeRouteLookup() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
|
|
|
+
|
|
|
+ verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void multipleCurrentPricingPeriodsFailExplicitlyBeforeRouteLookup() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt()))
|
|
|
+ .thenReturn(List.of(pricing(), pricing()));
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
|
|
|
+
|
|
|
+ verify(fixture.routeService, never()).calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void updatingPricingChecksOverlapBeforeVersionedWrite() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryPricing existing = pricing();
|
|
|
+ when(fixture.pricingMapper.selectById(1L)).thenReturn(existing);
|
|
|
+ when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
|
|
|
+ FlashDeliveryPricingRequest request = pricingRequest();
|
|
|
+ request.setConfigVersion(1);
|
|
|
+
|
|
|
+ fixture.service.updatePricing(9L, 1L, request);
|
|
|
+
|
|
|
+ InOrder writeOrder = inOrder(fixture.pricingMapper);
|
|
|
+ writeOrder.verify(fixture.pricingMapper)
|
|
|
+ .countOverlapping(eq("00:00"), eq("24:00"), anyInt(), eq(1L));
|
|
|
+ writeOrder.verify(fixture.pricingMapper).update(isNull(), any(Wrapper.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void deletePricingRemovesConfigurationImmediately() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectById(18L)).thenReturn(pricing());
|
|
|
+ when(fixture.pricingMapper.deleteById(18L)).thenReturn(1);
|
|
|
+
|
|
|
+ fixture.service.deletePricing(18L);
|
|
|
+
|
|
|
+ verify(fixture.pricingMapper).deleteById(18L);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void urgentQuoteReturnsSeparatedFeesAndUsesScheduledPickupTime() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(eq("16:30"), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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());
|
|
|
+ // 起步价与 45 元距离附加费分开返回,避免前端重复相加。
|
|
|
+ assertEquals(90L, quote.getBaseDeliveryFee());
|
|
|
+ assertEquals(27L, quote.getUrgentFee());
|
|
|
+ assertEquals(5L, quote.getTipAmount());
|
|
|
+ assertEquals(167L, quote.getAmount());
|
|
|
+ verify(fixture.pricingMapper).selectAtTime(eq("16:30"), anyInt());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createRejectsChangedQuoteAndReturnsLatestQuoteWithoutInsert() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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 createDefaultsPayTypeToCashAndStoresTransferSelection() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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());
|
|
|
+
|
|
|
+ var defaultResult = fixture.service.create(7L, createRequest());
|
|
|
+ FlashDeliveryCreateRequest transfer = createRequest();
|
|
|
+ transfer.setPayType("6");
|
|
|
+ var transferResult = fixture.service.create(7L, transfer);
|
|
|
+
|
|
|
+ ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
|
|
|
+ verify(fixture.orderMapper, times(2)).insert(saved.capture());
|
|
|
+ assertEquals("4", saved.getAllValues().get(0).getPayType());
|
|
|
+ assertEquals("6", saved.getAllValues().get(1).getPayType());
|
|
|
+ assertEquals("4", defaultResult.getPayType());
|
|
|
+ assertEquals("6", transferResult.getPayType());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createRejectsUnsupportedPayType() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
+ FlashDeliveryCreateRequest request = createRequest();
|
|
|
+ request.setPayType("2");
|
|
|
+ org.mockito.Mockito.doThrow(new ServiceException("flash.delivery.pay.type.invalid"))
|
|
|
+ .when(fixture.paymentMethodGateService).assertUsable(eq("2"),
|
|
|
+ eq(com.ruoyi.system.service.PaymentMethodGateService.GateScope.RIDER_FLASH), isNull(), isNull());
|
|
|
+
|
|
|
+ assertThrows(ServiceException.class, () -> fixture.service.create(7L, request));
|
|
|
+
|
|
|
+ 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(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt())).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 quoteAllowsMissingWeightRange() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(GeoPoint.class), any(GeoPoint.class), anyInt()))
|
|
|
+ .thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
+
|
|
|
+ FlashDeliveryQuoteRequest request = request();
|
|
|
+ request.setWeightRange(null);
|
|
|
+
|
|
|
+ assertDoesNotThrow(() -> fixture.service.quote(request));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void quoteRejectsUnknownWeightRangeWhenProvided() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ for (String weightRange : new String[]{"", "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.setVehicleType(1);
|
|
|
+ request.setPackageType("DOCUMENT");
|
|
|
+ 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");
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryCreateRequest createRequest() {
|
|
|
+ FlashDeliveryCreateRequest request = new FlashDeliveryCreateRequest();
|
|
|
+ FlashDeliveryQuoteRequest quote = request();
|
|
|
+ request.setServiceType(quote.getServiceType());
|
|
|
+ request.setDeliveryType(quote.getDeliveryType());
|
|
|
+ request.setVehicleType(quote.getVehicleType());
|
|
|
+ request.setPackageType(quote.getPackageType());
|
|
|
+ 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");
|
|
|
+ request.setDeliveryMode("SCHEDULED");
|
|
|
+ Date start = new Date(System.currentTimeMillis() + 60L * 60L * 1000L);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void prepareCreate(Fixture fixture) {
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(), any(), anyInt()))
|
|
|
+ .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));
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryAddressRequest address(String name, String phone, String lat, String lon) {
|
|
|
+ FlashDeliveryAddressRequest address = new FlashDeliveryAddressRequest();
|
|
|
+ address.setName(name);
|
|
|
+ address.setPhone(phone);
|
|
|
+ address.setAddress("road");
|
|
|
+ address.setAddressDetail("door");
|
|
|
+ address.setLatitude(new BigDecimal(lat));
|
|
|
+ address.setLongitude(new BigDecimal(lon));
|
|
|
+ return address;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryPricing pricing() {
|
|
|
+ FlashDeliveryPricing pricing = new FlashDeliveryPricing();
|
|
|
+ pricing.setId(1L);
|
|
|
+ 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.setVehicleType(1);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings({"rawtypes", "unchecked"})
|
|
|
+ private void assertRiderTab(String tab, List<String> statuses) {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ rider(fixture, 8L);
|
|
|
+ when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
|
|
|
+ any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
|
|
|
+
|
|
|
+ fixture.service.riderOrders(8L, 1, 10, tab, null, null);
|
|
|
+
|
|
|
+ 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());
|
|
|
+ // MyBatis-Plus 的条件参数在渲染 SQL 段时才写入参数表,必须先取 SQL 再断言参数。
|
|
|
+ String sql = query.getValue().getSqlSegment();
|
|
|
+ assertTrue(sql.contains("rider_id"), tab);
|
|
|
+ var parameters = query.getValue().getParamNameValuePairs().values();
|
|
|
+ assertTrue(parameters.contains(8L), tab);
|
|
|
+ for (String status : statuses) assertTrue(parameters.contains(status), tab + ":" + status);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void rider(Fixture fixture, Long riderId) {
|
|
|
+ InfoUser rider = new InfoUser();
|
|
|
+ rider.setUserId(riderId);
|
|
|
+ rider.setUserType("2");
|
|
|
+ rider.setDeliveryType("FLASH");
|
|
|
+ when(fixture.userMapper.selectInfoUserByUserId(riderId)).thenReturn(rider);
|
|
|
+ }
|
|
|
+
|
|
|
+ private com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest rating(Double stars) {
|
|
|
+ com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest request =
|
|
|
+ new com.ruoyi.app.flashdelivery.dto.FlashDeliveryRatingRequest();
|
|
|
+ request.setStars(stars);
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryOrder order(Long id, String status) {
|
|
|
+ FlashDeliveryOrder order = new FlashDeliveryOrder();
|
|
|
+ order.setId(id);
|
|
|
+ order.setPickupStopCount(1); order.setPickedUpStopCount(0); order.setVersion(0);
|
|
|
+ order.setOrderNo("FL-20260813-021");
|
|
|
+ order.setUserId(7L);
|
|
|
+ order.setServiceType("HELP_SEND");
|
|
|
+ order.setDeliveryType("NORMAL");
|
|
|
+ order.setStatus(status);
|
|
|
+ order.setIsDisplay(true);
|
|
|
+ order.setPackageType("DOCUMENT");
|
|
|
+ order.setQuantity(1);
|
|
|
+ order.setWeightRange("UP_TO_5_KG");
|
|
|
+ order.setDeliveryMode("NOW");
|
|
|
+ order.setPinRequired(true);
|
|
|
+ order.setDeliveryPinCode("4821");
|
|
|
+ order.setPickupName("pickup contact");
|
|
|
+ order.setPickupPhone("0911111111");
|
|
|
+ order.setPickupAddress("pickup road");
|
|
|
+ order.setPickupAddressDetail("pickup door");
|
|
|
+ order.setPickupCity("台北市");
|
|
|
+ order.setPickupArea("中山區");
|
|
|
+ order.setPickupLongitude(new BigDecimal("121.5100"));
|
|
|
+ order.setPickupLatitude(new BigDecimal("25.0100"));
|
|
|
+ order.setDeliveryName("delivery contact");
|
|
|
+ order.setDeliveryPhone("0922222222");
|
|
|
+ order.setDeliveryAddress("delivery road");
|
|
|
+ order.setDeliveryAddressDetail("delivery door");
|
|
|
+ order.setDeliveryCity("台北市");
|
|
|
+ order.setDeliveryArea("大安區");
|
|
|
+ order.setDeliveryLongitude(new BigDecimal("121.5200"));
|
|
|
+ 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");
|
|
|
+ order.setCreateTime(new Date());
|
|
|
+ return order;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlashDeliveryOrderImage image(String proofType, String url) {
|
|
|
+ FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
|
|
|
+ image.setProofType(proofType);
|
|
|
+ image.setImageUrl(url);
|
|
|
+ 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 addressQuoteUsesOriginalPricingSnapshotAndDoesNotWrite() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ FlashDeliveryOrder order = editableOrder(fixture);
|
|
|
+ // FR-074/093:预约单也保留原运价快照,平台调价不得改变旧单的计价参数。
|
|
|
+ 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(90L, 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(), anyInt());
|
|
|
+ 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(GeoPoint.class), any(GeoPoint.class), anyInt());
|
|
|
+ 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(GeoPoint.class), any(GeoPoint.class), anyInt())).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(GeoPoint.class), any(GeoPoint.class), anyInt());
|
|
|
+ verify(fixture.pricingMapper, never()).selectAtTime(anyString(), anyInt());
|
|
|
+ }
|
|
|
+
|
|
|
+ @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(GeoPoint.class), any(GeoPoint.class), anyInt())).thenReturn(new RouteDistance(4500, 900, "GOOGLE"));
|
|
|
+ // 后台现价与快照不同;改址仍以订单保存的价格计算。
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void generateOrderNoUsesTimestampAndSequenceFormat() {
|
|
|
+ String first = FlashDeliveryApplicationService.generateOrderNo();
|
|
|
+ String second = FlashDeliveryApplicationService.generateOrderNo();
|
|
|
+
|
|
|
+ assertTrue(isFlashOrderNo(first), "应为 98 前缀 17 位纯数字,实际: " + first);
|
|
|
+ assertTrue(isFlashOrderNo(second), "应为 98 前缀 17 位纯数字,实际: " + second);
|
|
|
+ assertNotEquals(first, second, "同毫秒连续生成应由序列位区分,不得同号");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createRegeneratesOrderNoWhenUniqueKeyConflicts() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(), any(), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
+ when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone(anyString())).thenReturn(List.of());
|
|
|
+ org.springframework.dao.DataIntegrityViolationException orderNoConflict =
|
|
|
+ new org.springframework.dao.DataIntegrityViolationException("insert failed",
|
|
|
+ new java.sql.SQLException(
|
|
|
+ "Duplicate entry '98001' for key 'flash_delivery_order.uk_flash_order_no'"));
|
|
|
+ // 捕获器拿到的是同一对象引用,重试改单号后两次取值相同;改为每次调用时记录当时单号
|
|
|
+ java.util.List<String> attempted = new java.util.ArrayList<>();
|
|
|
+ doAnswer(invocation -> {
|
|
|
+ FlashDeliveryOrder order = invocation.getArgument(0);
|
|
|
+ attempted.add(order.getOrderNo());
|
|
|
+ if (attempted.size() == 1) throw orderNoConflict;
|
|
|
+ order.setId(99L);
|
|
|
+ return 1;
|
|
|
+ }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
|
|
|
+
|
|
|
+ fixture.service.create(7L, createRequest());
|
|
|
+
|
|
|
+ verify(fixture.orderMapper, times(2)).insert(any(FlashDeliveryOrder.class));
|
|
|
+ assertEquals(2, attempted.size());
|
|
|
+ assertNotEquals(attempted.get(0), attempted.get(1), "撞号重试后应换新单号");
|
|
|
+ assertTrue(isFlashOrderNo(attempted.get(1)), "重试后仍应为新格式单号");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void createKeepsIdempotencyFallbackForNonOrderNoConflicts() {
|
|
|
+ Fixture fixture = new Fixture();
|
|
|
+ when(fixture.pricingMapper.selectAtTime(anyString(), anyInt())).thenReturn(List.of(pricing()));
|
|
|
+ when(fixture.routeService.calculate(any(), any(), anyInt())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
|
|
|
+ when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone(anyString())).thenReturn(List.of());
|
|
|
+ org.springframework.dao.DataIntegrityViolationException idempotentConflict =
|
|
|
+ new org.springframework.dao.DataIntegrityViolationException("insert failed",
|
|
|
+ new java.sql.SQLException(
|
|
|
+ "Duplicate entry '7-req-1' for key 'flash_delivery_order.uk_user_request'"));
|
|
|
+ doThrow(idempotentConflict).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
|
|
|
+ // 前置幂等查询放行(null);插入冲突后 catch 内按 v1 实现走 FOR UPDATE 复查 + lockOrder 锁单
|
|
|
+ FlashDeliveryOrder concurrent = new FlashDeliveryOrder();
|
|
|
+ concurrent.setId(55L);
|
|
|
+ concurrent.setOrderNo("981789000000011");
|
|
|
+ when(fixture.orderMapper.selectByUserRequestId(eq(7L), anyString())).thenReturn(null);
|
|
|
+ when(fixture.orderMapper.selectOne(any(Wrapper.class))).thenReturn(concurrent);
|
|
|
+ when(fixture.orderMapper.selectById(55L)).thenReturn(concurrent);
|
|
|
+
|
|
|
+ var detail = fixture.service.create(7L, createRequest());
|
|
|
+
|
|
|
+ assertEquals(55L, detail.getId());
|
|
|
+ verify(fixture.orderMapper, times(1)).insert(any(FlashDeliveryOrder.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 闪送新格式单号:98 开头、17 位纯数字。 */
|
|
|
+ private static boolean isFlashOrderNo(String orderNo) {
|
|
|
+ return orderNo != null && orderNo.startsWith("98") && orderNo.length() == 17
|
|
|
+ && orderNo.chars().allMatch(Character::isDigit);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class Fixture {
|
|
|
+ final com.ruoyi.system.mapper.flash.FlashDeliveryOrderStopMapper stopMapper = mock(com.ruoyi.system.mapper.flash.FlashDeliveryOrderStopMapper.class);
|
|
|
+ final com.ruoyi.system.mapper.flash.FlashDeliveryTargetAdjustmentMapper adjustmentMapper = mock(com.ruoyi.system.mapper.flash.FlashDeliveryTargetAdjustmentMapper.class);
|
|
|
+ final FlashDeliveryOrderMapper orderMapper = mock(FlashDeliveryOrderMapper.class);
|
|
|
+ final FlashDeliveryPricingMapper pricingMapper = mock(FlashDeliveryPricingMapper.class);
|
|
|
+ final FlashDeliveryOrderImageMapper imageMapper = mock(FlashDeliveryOrderImageMapper.class);
|
|
|
+ final FlashDeliveryOrderLogMapper logMapper = mock(FlashDeliveryOrderLogMapper.class);
|
|
|
+ final InfoUserMapper userMapper = mock(InfoUserMapper.class);
|
|
|
+ final com.ruoyi.system.mapper.InfoBankCardMapper bankCardMapper = mock(com.ruoyi.system.mapper.InfoBankCardMapper.class);
|
|
|
+ final FlashDeliveryRouteService routeService = mock(FlashDeliveryRouteService.class);
|
|
|
+ final RiderDeliveryLockService lockService = mock(RiderDeliveryLockService.class);
|
|
|
+ final RiderDeliveryAcceptanceService acceptanceService = mock(RiderDeliveryAcceptanceService.class);
|
|
|
+ final com.ruoyi.system.service.PaymentMethodGateService paymentMethodGateService =
|
|
|
+ mock(com.ruoyi.system.service.PaymentMethodGateService.class);
|
|
|
+ final FlashDeliveryNotificationService notificationService = mock(FlashDeliveryNotificationService.class);
|
|
|
+ final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(
|
|
|
+ orderMapper, pricingMapper, imageMapper, logMapper, userMapper, bankCardMapper,
|
|
|
+ routeService, new FlashDeliveryPricingCalculator(), lockService, acceptanceService, stopMapper, adjustmentMapper,
|
|
|
+ paymentMethodGateService, notificationService);
|
|
|
+
|
|
|
+ Fixture() {
|
|
|
+ when(paymentMethodGateService.listAvailable(any(), any(), any())).thenReturn(List.of(
|
|
|
+ new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("COD", "1", true),
|
|
|
+ new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("LINE_PAY", "3", true),
|
|
|
+ new com.ruoyi.system.service.PaymentMethodGateService.PayMethodOption("OFFLINE_TRANSFER", "6", true)));
|
|
|
+ // Existing single-stop scenarios now explicitly provide station rows; production never synthesizes legacy stops.
|
|
|
+ when(orderMapper.selectByIdForUpdate(any())).thenAnswer(i -> {
|
|
|
+ FlashDeliveryOrder current = orderMapper.selectById((Long) i.getArgument(0));
|
|
|
+ if (current != null && current.getPickupStopCount() == null) current.setPickupStopCount(1);
|
|
|
+ return current;
|
|
|
+ });
|
|
|
+ when(stopMapper.insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderStop.class))).thenReturn(1);
|
|
|
+ when(stopMapper.updateById(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderStop.class))).thenReturn(1);
|
|
|
+ when(imageMapper.insert(any(FlashDeliveryOrderImage.class))).thenReturn(1);
|
|
|
+ when(stopMapper.selectList(any())).thenAnswer(i -> {
|
|
|
+ FlashDeliveryOrder current = orderMapper.selectById(10L);
|
|
|
+ var pickup = new com.ruoyi.system.domain.flash.FlashDeliveryOrderStop();
|
|
|
+ pickup.setId(101L); pickup.setOrderId(10L); pickup.setStopType("PICKUP"); pickup.setStopOrder(0);
|
|
|
+ pickup.setName("pickup contact"); pickup.setPhone("0911111111"); pickup.setAddress("pickup road");
|
|
|
+ pickup.setLatitude(new BigDecimal("25.0100")); pickup.setLongitude(new BigDecimal("121.5100"));
|
|
|
+ pickup.setPackageType("DOCUMENT"); pickup.setQuantity(1); pickup.setWeightRange("UP_TO_5_KG");
|
|
|
+ pickup.setStatus(current != null && List.of("PICKED_UP", "DELIVERED", "COMPLETED").contains(current.getStatus())
|
|
|
+ ? "PICKED_UP" : "PENDING");
|
|
|
+ var delivery = new com.ruoyi.system.domain.flash.FlashDeliveryOrderStop();
|
|
|
+ delivery.setId(102L); delivery.setOrderId(10L); delivery.setStopType("DELIVERY"); delivery.setStopOrder(1); delivery.setStatus("PENDING");
|
|
|
+ return List.of(pickup, delivery);
|
|
|
+ });
|
|
|
+ when(lockService.withLock(any(), any())).thenAnswer(invocation -> {
|
|
|
+ RiderDeliveryLockService.LockedCall<?> call = invocation.getArgument(1);
|
|
|
+ return call.call();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|