FlashDeliveryApplicationServiceTest.java 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418
  1. package com.ruoyi.app.flashdelivery.service;
  2. import com.baomidou.mybatisplus.core.conditions.Wrapper;
  3. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  4. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressRequest;
  5. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressChangeRequest;
  6. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryAddressConfirmRequest;
  7. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryTipAddRequest;
  8. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryQuoteView;
  9. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryCreateRequest;
  10. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryProofRequest;
  11. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryPricingRequest;
  12. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryQuoteRequest;
  13. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryDeliverRequest;
  14. import com.ruoyi.app.flashdelivery.dto.FlashDeliveryRiderOrderPageView;
  15. import com.ruoyi.app.flashdelivery.exception.FlashDeliveryQuoteChangedException;
  16. import com.ruoyi.app.flashdelivery.route.FlashDeliveryRouteService;
  17. import com.ruoyi.app.flashdelivery.route.RouteDistance;
  18. import com.ruoyi.app.order.RiderDeliveryExclusivityService;
  19. import com.ruoyi.app.order.RiderDeliveryLockService;
  20. import com.ruoyi.system.domain.InfoUser;
  21. import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
  22. import com.ruoyi.system.domain.flash.FlashDeliveryOrderImage;
  23. import com.ruoyi.system.domain.flash.FlashDeliveryPricing;
  24. import com.ruoyi.system.mapper.InfoUserMapper;
  25. import com.ruoyi.system.mapper.flash.FlashDeliveryOrderImageMapper;
  26. import com.ruoyi.system.mapper.flash.FlashDeliveryOrderLogMapper;
  27. import com.ruoyi.system.mapper.flash.FlashDeliveryOrderMapper;
  28. import com.ruoyi.system.mapper.flash.FlashDeliveryPricingMapper;
  29. import org.junit.jupiter.api.Test;
  30. import org.junit.jupiter.api.BeforeAll;
  31. import org.junit.jupiter.api.AfterAll;
  32. import org.mockito.ArgumentCaptor;
  33. import org.mockito.InOrder;
  34. import org.mockito.MockedStatic;
  35. import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  36. import org.springframework.beans.factory.support.DefaultListableBeanFactory;
  37. import org.springframework.context.support.StaticMessageSource;
  38. import com.ruoyi.common.core.domain.entity.SysDictData;
  39. import com.ruoyi.common.utils.DictUtils;
  40. import com.ruoyi.common.utils.spring.SpringUtils;
  41. import org.springframework.test.util.ReflectionTestUtils;
  42. import java.math.BigDecimal;
  43. import java.util.List;
  44. import java.util.Date;
  45. import java.util.Calendar;
  46. import static com.ruoyi.system.domain.flash.FlashDeliveryStatus.ACCEPTED;
  47. import static org.junit.jupiter.api.Assertions.assertEquals;
  48. import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
  49. import static org.junit.jupiter.api.Assertions.assertFalse;
  50. import static org.junit.jupiter.api.Assertions.assertNull;
  51. import static org.junit.jupiter.api.Assertions.assertThrows;
  52. import static org.junit.jupiter.api.Assertions.assertTrue;
  53. import com.ruoyi.common.exception.ServiceException;
  54. import static org.mockito.ArgumentMatchers.any;
  55. import static org.mockito.ArgumentMatchers.anyString;
  56. import static org.mockito.ArgumentMatchers.eq;
  57. import static org.mockito.ArgumentMatchers.isNull;
  58. import static org.mockito.Mockito.mock;
  59. import static org.mockito.Mockito.mockStatic;
  60. import static org.mockito.Mockito.never;
  61. import static org.mockito.Mockito.times;
  62. import static org.mockito.Mockito.verify;
  63. import static org.mockito.Mockito.when;
  64. import static org.mockito.Mockito.doAnswer;
  65. import static org.mockito.Mockito.inOrder;
  66. import static org.mockito.Mockito.doThrow;
  67. class FlashDeliveryApplicationServiceTest {
  68. private static ConfigurableListableBeanFactory originalBeanFactory;
  69. @BeforeAll
  70. static void initializeMessages() {
  71. originalBeanFactory = (ConfigurableListableBeanFactory)
  72. ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
  73. DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
  74. StaticMessageSource messageSource = new StaticMessageSource();
  75. messageSource.setUseCodeAsDefaultMessage(true);
  76. beanFactory.registerSingleton("messageSource", messageSource);
  77. new SpringUtils().postProcessBeanFactory(beanFactory);
  78. }
  79. @AfterAll
  80. static void restoreMessages() {
  81. new SpringUtils().postProcessBeanFactory(originalBeanFactory);
  82. }
  83. @Test
  84. void quoteUsesServerRouteAndCurrentTimePricing() {
  85. Fixture fixture = new Fixture();
  86. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  87. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3001, 600, "ROUTE"));
  88. var quote = fixture.service.quote(request());
  89. assertEquals(3001, quote.getDistanceMeters());
  90. assertEquals(90L, quote.getAmount());
  91. assertEquals(1L, quote.getPricingId());
  92. assertEquals("00:00", quote.getStartTime());
  93. assertEquals("24:00", quote.getEndTime());
  94. assertEquals("ROUTE", quote.getDistanceSource());
  95. }
  96. @Test
  97. void quoteRejectsRoutesLongerThanFortyKilometres() {
  98. Fixture fixture = new Fixture();
  99. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  100. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(40001, 1800, "ROUTE"));
  101. assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
  102. }
  103. @Test
  104. void wrongDeliveryPinCannotPersistProofOrChangeState() {
  105. Fixture fixture = new Fixture();
  106. InfoUser rider = new InfoUser();
  107. rider.setUserId(8L);
  108. rider.setUserType("2");
  109. when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
  110. FlashDeliveryOrder order = new FlashDeliveryOrder();
  111. order.setId(10L);
  112. order.setRiderId(8L);
  113. order.setStatus("PICKED_UP");
  114. order.setPinRequired(true);
  115. order.setDeliveryPinCode("4821");
  116. when(fixture.orderMapper.selectById(10L)).thenReturn(order);
  117. FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
  118. request.setPinCode("1234");
  119. request.setImageUrls(List.of("https://example.com/delivery.jpg"));
  120. assertThrows(ServiceException.class, () -> fixture.service.deliver(8L, 10L, request));
  121. verify(fixture.imageMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class));
  122. verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
  123. }
  124. @Test
  125. void correctDeliveryPinPersistsDeliveryProofAndChangesState() {
  126. Fixture fixture = new Fixture();
  127. InfoUser rider = new InfoUser();
  128. rider.setUserId(8L);
  129. rider.setUserType("2");
  130. when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
  131. FlashDeliveryOrder order = new FlashDeliveryOrder();
  132. order.setId(10L);
  133. order.setRiderId(8L);
  134. order.setStatus("PICKED_UP");
  135. order.setPinRequired(true);
  136. order.setDeliveryPinCode("4821");
  137. when(fixture.orderMapper.selectById(10L)).thenReturn(order);
  138. when(fixture.orderMapper.transitionByRider(eq(10L), eq(8L), eq("PICKED_UP"),
  139. eq("DELIVERED"), eq("delivered_at"), any())).thenReturn(1);
  140. FlashDeliveryDeliverRequest request = new FlashDeliveryDeliverRequest();
  141. request.setPinCode("4821");
  142. request.setImageUrls(List.of("https://example.com/delivery.jpg"));
  143. fixture.service.deliver(8L, 10L, request);
  144. ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
  145. ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
  146. verify(fixture.imageMapper).insert(image.capture());
  147. assertEquals("DELIVERY", image.getValue().getProofType());
  148. assertEquals("RIDER", image.getValue().getOperatorType());
  149. }
  150. @Test
  151. void scheduledCreateGeneratesUserOnlyPinAndStoresRelativeSenderProof() {
  152. Fixture fixture = new Fixture();
  153. when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
  154. .thenReturn(List.of(88L));
  155. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  156. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
  157. doAnswer(invocation -> {
  158. FlashDeliveryOrder order = invocation.getArgument(0);
  159. order.setId(99L);
  160. return 1;
  161. }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
  162. when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
  163. when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
  164. FlashDeliveryCreateRequest request = createRequest();
  165. request.setSenderImageUrls(List.of("/profile/upload/2026/09/07/sender.jpg"));
  166. var result = fixture.service.create(7L, request);
  167. assertEquals(4, result.getDeliveryPinCode().length());
  168. ArgumentCaptor<com.ruoyi.system.domain.flash.FlashDeliveryOrderImage> image =
  169. ArgumentCaptor.forClass(com.ruoyi.system.domain.flash.FlashDeliveryOrderImage.class);
  170. verify(fixture.imageMapper).insert(image.capture());
  171. assertEquals("SENDER", image.getValue().getProofType());
  172. assertEquals("USER", image.getValue().getOperatorType());
  173. assertEquals("/profile/upload/2026/09/07/sender.jpg", image.getValue().getImageUrl());
  174. ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
  175. verify(fixture.orderMapper).insert(saved.capture());
  176. assertEquals("SCHEDULED", saved.getValue().getDeliveryMode());
  177. assertEquals("NORMAL", saved.getValue().getDeliveryType());
  178. assertEquals("DOCUMENT", saved.getValue().getPackageType());
  179. assertEquals(1, saved.getValue().getQuantity());
  180. assertEquals("UP_TO_5_KG", saved.getValue().getWeightRange());
  181. assertEquals("30 x 20 x 10 cm", saved.getValue().getSpecification());
  182. assertEquals(88L, saved.getValue().getReceiverUserId());
  183. assertEquals(1L, saved.getValue().getPricingId());
  184. assertEquals("00:00", saved.getValue().getPricingStartTime());
  185. assertEquals("24:00", saved.getValue().getPricingEndTime());
  186. assertEquals(new BigDecimal("3.00"), saved.getValue().getStartingDistance());
  187. assertEquals(90L, saved.getValue().getStartingFare());
  188. assertEquals(new BigDecimal("1.00"), saved.getValue().getDistance());
  189. assertEquals(15L, saved.getValue().getFreight());
  190. assertEquals(0L, saved.getValue().getDistanceFee());
  191. assertEquals(90L, saved.getValue().getBaseDeliveryFee());
  192. assertEquals(new BigDecimal("20.00"), saved.getValue().getUrgentRate());
  193. assertEquals(10L, saved.getValue().getMinimumUrgentFee());
  194. assertEquals(0L, saved.getValue().getUrgentFee());
  195. assertEquals(0L, saved.getValue().getTipAmount());
  196. }
  197. @Test
  198. @SuppressWarnings({"rawtypes", "unchecked"})
  199. void userListOnlyFiltersByOwnerAndReturnsCardFields() {
  200. Fixture fixture = new Fixture();
  201. FlashDeliveryOrder order = order(10L, "COMPLETED");
  202. order.setWeightRange("OVER_5_TO_10_KG");
  203. Page<FlashDeliveryOrder> source = new Page<>(1, 10, 1);
  204. source.setRecords(List.of(order));
  205. when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  206. any(Wrapper.class))).thenReturn(source);
  207. var result = fixture.service.userOrders(7L, 1, 10, null);
  208. ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
  209. ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
  210. verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
  211. String sql = query.getValue().getSqlSegment().toLowerCase();
  212. var parameters = query.getValue().getParamNameValuePairs().values();
  213. assertTrue(sql.contains("user_id"));
  214. assertFalse(sql.contains("status"));
  215. assertFalse(sql.contains("service_type"));
  216. assertTrue(parameters.contains(7L));
  217. assertEquals("pickup road", result.getRecords().get(0).getPickupAddress());
  218. assertEquals("OVER_5_TO_10_KG", result.getRecords().get(0).getWeightRange());
  219. assertEquals(112L, result.getRecords().get(0).getAmount());
  220. }
  221. @Test
  222. void createDoesNotBindReceiverWhenNormalizedPhoneMatchesMultipleUsers() {
  223. Fixture fixture = new Fixture();
  224. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  225. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
  226. when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("886912345678"))
  227. .thenReturn(List.of(88L, 99L));
  228. doAnswer(invocation -> {
  229. FlashDeliveryOrder order = invocation.getArgument(0);
  230. order.setId(99L);
  231. return 1;
  232. }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
  233. FlashDeliveryCreateRequest request = createRequest();
  234. request.getDelivery().setPhone("+886 912-345-678");
  235. fixture.service.create(7L, request);
  236. ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
  237. verify(fixture.orderMapper).insert(saved.capture());
  238. assertNull(saved.getValue().getReceiverUserId());
  239. }
  240. @Test
  241. void createLeavesReceiverUnboundWhenPhoneHasNoRegisteredUser() {
  242. Fixture fixture = new Fixture();
  243. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  244. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
  245. when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
  246. .thenReturn(List.of());
  247. doAnswer(invocation -> {
  248. FlashDeliveryOrder order = invocation.getArgument(0);
  249. order.setId(99L);
  250. return 1;
  251. }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
  252. fixture.service.create(7L, createRequest());
  253. ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
  254. verify(fixture.orderMapper).insert(saved.capture());
  255. assertNull(saved.getValue().getReceiverUserId());
  256. }
  257. @Test
  258. @SuppressWarnings({"rawtypes", "unchecked"})
  259. void receiverListFiltersByCreationTimeReceiverBinding() {
  260. Fixture fixture = new Fixture();
  261. when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  262. any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
  263. fixture.service.userOrders(88L, 1, 10, "receiver");
  264. ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
  265. ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
  266. verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
  267. assertTrue(query.getValue().getSqlSegment().toLowerCase().contains("receiver_user_id"));
  268. assertTrue(query.getValue().getParamNameValuePairs().values().contains(88L));
  269. }
  270. @Test
  271. @SuppressWarnings({"rawtypes", "unchecked"})
  272. void riderNewTaskUsesFoodOrderParametersAndReturnsPrototypeCardSummary() {
  273. Fixture fixture = new Fixture();
  274. rider(fixture, 8L);
  275. FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
  276. order.setWeightRange("OVER_10_TO_15_KG");
  277. Page<FlashDeliveryOrder> source = new Page<>(1, 10, 1);
  278. source.setRecords(List.of(order));
  279. when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  280. any(Wrapper.class))).thenReturn(source);
  281. when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of(168L));
  282. SysDictData distanceLimit = new SysDictData();
  283. distanceLimit.setDictValue("5");
  284. FlashDeliveryRiderOrderPageView result;
  285. try (MockedStatic<DictUtils> dictionary = mockStatic(DictUtils.class)) {
  286. dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance"))
  287. .thenReturn(List.of(distanceLimit));
  288. result = fixture.service.riderOrders(8L, 1, 10, "newTask",
  289. new BigDecimal("121.5000"), new BigDecimal("25.0000"));
  290. }
  291. assertEquals(1L, result.getNearbyTaskCount());
  292. assertEquals(168L, result.getHighestOrderAmount());
  293. assertEquals("pickup road", result.getRecords().get(0).getPickupAddress());
  294. assertEquals("delivery road", result.getRecords().get(0).getDeliveryAddress());
  295. assertEquals("OVER_10_TO_15_KG", result.getRecords().get(0).getWeightRange());
  296. assertTrue(result.getRecords().get(0).getPickupDistanceMeters() > 0);
  297. assertTrue(result.getRecords().get(0).getPinRequired());
  298. ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
  299. ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
  300. verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
  301. String sql = query.getValue().getSqlSegment().toLowerCase();
  302. var parameters = query.getValue().getParamNameValuePairs().values();
  303. assertTrue(sql.contains("status"));
  304. assertTrue(sql.contains("rider_id is null"));
  305. assertTrue(sql.contains("scheduled_pickup_start_at"));
  306. assertTrue(sql.contains("st_distance_sphere"));
  307. assertTrue(parameters.contains("WAITING_ACCEPTANCE"));
  308. assertTrue(parameters.contains("NOW"));
  309. assertTrue(parameters.stream().anyMatch(Date.class::isInstance));
  310. ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> summaryQuery =
  311. ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
  312. verify(fixture.orderMapper).selectObjs(summaryQuery.capture());
  313. String summarySql = summaryQuery.getValue().getSqlSegment().toLowerCase();
  314. var summaryParameters = summaryQuery.getValue().getParamNameValuePairs().values();
  315. assertTrue(summarySql.contains("rider_id is null"));
  316. assertTrue(summarySql.contains("scheduled_pickup_start_at"));
  317. assertTrue(summarySql.contains("st_distance_sphere"));
  318. assertTrue(summaryParameters.contains("WAITING_ACCEPTANCE"));
  319. }
  320. @Test
  321. @SuppressWarnings({"rawtypes", "unchecked"})
  322. void riderNewTaskWithoutPositionFallsBackToNewestFirst() {
  323. Fixture fixture = new Fixture();
  324. rider(fixture, 8L);
  325. when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  326. any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
  327. when(fixture.orderMapper.selectObjs(any(Wrapper.class))).thenReturn(List.of());
  328. fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
  329. ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
  330. ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
  331. verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
  332. String sql = query.getValue().getSqlSegment().toLowerCase();
  333. assertTrue(sql.contains("order by create_time desc"));
  334. assertFalse(sql.contains("st_distance_sphere"));
  335. }
  336. @Test
  337. @SuppressWarnings({"rawtypes", "unchecked"})
  338. void riderTabsMapStatusesBeforePagingAndRestrictAssignedTabsToRider() {
  339. assertRiderTab("toPickup", List.of("ACCEPTED"));
  340. assertRiderTab("delivering", List.of("PICKED_UP"));
  341. assertRiderTab("completed", List.of("DELIVERED", "COMPLETED"));
  342. assertRiderTab("cancelled", List.of("CANCELLED"));
  343. }
  344. @Test
  345. void riderListRejectsUnknownTabAndUnpairedOrInvalidCoordinates() {
  346. Fixture fixture = new Fixture();
  347. rider(fixture, 8L);
  348. assertThrows(ServiceException.class,
  349. () -> fixture.service.riderOrders(8L, 1, 10, "refund", null, null));
  350. assertThrows(ServiceException.class,
  351. () -> fixture.service.riderOrders(8L, 1, 10, null, null, null));
  352. assertThrows(ServiceException.class,
  353. () -> fixture.service.riderOrders(8L, 1, 10, "newTask", BigDecimal.ONE, null));
  354. assertThrows(ServiceException.class,
  355. () -> fixture.service.riderOrders(8L, 1, 10, "newTask",
  356. new BigDecimal("181"), BigDecimal.ZERO));
  357. verify(fixture.orderMapper, never()).selectPage(any(), any());
  358. }
  359. @Test
  360. void flashNewTaskListAndAcceptRequireFlashDeliveryType() {
  361. Fixture fixture = new Fixture();
  362. InfoUser rider = new InfoUser();
  363. rider.setUserId(8L);
  364. rider.setUserType("2");
  365. rider.setDeliveryType("FOOD");
  366. when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
  367. assertThrows(ServiceException.class,
  368. () -> fixture.service.riderOrders(8L, 1, 10, "newTask", null, null));
  369. assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
  370. verify(fixture.orderMapper, never()).selectPage(any(), any());
  371. verify(fixture.orderMapper, never()).accept(any(), any(), any());
  372. }
  373. @Test
  374. void riderDetailHidesWaitingFlashOrdersFromNonFlashRiders() {
  375. Fixture fixture = new Fixture();
  376. InfoUser rider = new InfoUser();
  377. rider.setUserId(8L);
  378. rider.setUserType("2");
  379. rider.setDeliveryType("FOOD");
  380. when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
  381. when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
  382. assertThrows(ServiceException.class, () -> fixture.service.riderDetail(8L, 10L));
  383. verify(fixture.imageMapper, never()).selectList(any());
  384. }
  385. @Test
  386. @SuppressWarnings({"rawtypes", "unchecked"})
  387. void enabledFlashRiderStillListsNewTasks() {
  388. Fixture fixture = new Fixture();
  389. InfoUser rider = new InfoUser();
  390. rider.setUserId(8L);
  391. rider.setUserType("2");
  392. rider.setDeliveryType("FLASH");
  393. when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
  394. when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  395. any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
  396. fixture.service.riderOrders(8L, 1, 10, "newTask", null, null);
  397. verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  398. any(Wrapper.class));
  399. }
  400. @Test
  401. @SuppressWarnings({"rawtypes", "unchecked"})
  402. void ownOrderTabsRemainAccessibleRegardlessOfDeliveryType() {
  403. Fixture fixture = new Fixture();
  404. InfoUser rider = new InfoUser();
  405. rider.setUserId(8L);
  406. rider.setUserType("2");
  407. rider.setDeliveryType("FOOD");
  408. when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
  409. when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  410. any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
  411. fixture.service.riderOrders(8L, 1, 10, "toPickup", null, null);
  412. verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  413. any(Wrapper.class));
  414. }
  415. @Test
  416. void assignedRiderTabsIgnoreLocationParameters() {
  417. Fixture fixture = new Fixture();
  418. rider(fixture, 8L);
  419. when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  420. any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
  421. fixture.service.riderOrders(8L, 1, 10, "toPickup",
  422. new BigDecimal("181"), new BigDecimal("91"));
  423. verify(fixture.orderMapper).selectPage(any(), any());
  424. }
  425. @Test
  426. void preAcceptRiderDetailIsDirectAndShowsTextAddressWithoutSensitiveFields() {
  427. Fixture fixture = new Fixture();
  428. rider(fixture, 8L);
  429. FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
  430. order.setWeightRange("OVER_15_TO_20_KG");
  431. when(fixture.orderMapper.selectById(10L)).thenReturn(order);
  432. var result = fixture.service.riderDetail(8L, 10L);
  433. assertEquals(10L, result.getId());
  434. assertEquals("pickup road", result.getPickup().getAddress());
  435. assertEquals("pickup door", result.getPickup().getAddressDetail());
  436. assertEquals("delivery road", result.getDelivery().getAddress());
  437. assertEquals("delivery door", result.getDelivery().getAddressDetail());
  438. assertEquals("OVER_15_TO_20_KG", result.getWeightRange());
  439. assertNull(result.getPickup().getName());
  440. assertNull(result.getPickup().getPhone());
  441. assertNull(result.getPickup().getLongitude());
  442. assertNull(result.getPickup().getLatitude());
  443. assertNull(result.getDelivery().getName());
  444. assertNull(result.getDelivery().getPhone());
  445. assertNull(result.getDelivery().getLongitude());
  446. assertNull(result.getDelivery().getLatitude());
  447. assertNull(result.getDeliveryPinCode());
  448. assertNull(result.getUserNote());
  449. assertTrue(result.getSenderImageUrls().isEmpty());
  450. assertTrue(result.getPickupImageUrls().isEmpty());
  451. assertTrue(result.getDeliveryImageUrls().isEmpty());
  452. verify(fixture.imageMapper, never()).selectList(any());
  453. verify(fixture.logMapper, never()).selectList(any());
  454. }
  455. @Test
  456. void userDetailReturnsDirectOrderAndGroupsImageUrlsWithoutRawLogs() {
  457. Fixture fixture = new Fixture();
  458. FlashDeliveryOrder order = order(10L, ACCEPTED);
  459. order.setRiderId(null);
  460. order.setWeightRange("OVER_5_TO_10_KG");
  461. when(fixture.orderMapper.selectById(10L)).thenReturn(order);
  462. when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of(
  463. image("SENDER", "https://example.com/sender.jpg"),
  464. image("PICKUP", "https://example.com/pickup.jpg"),
  465. image("DELIVERY", "https://example.com/delivery.jpg")));
  466. var result = fixture.service.userDetail(7L, 10L);
  467. assertEquals(10L, result.getId());
  468. assertEquals(List.of("https://example.com/sender.jpg"), result.getSenderImageUrls());
  469. assertEquals(List.of("https://example.com/pickup.jpg"), result.getPickupImageUrls());
  470. assertEquals(List.of("https://example.com/delivery.jpg"), result.getDeliveryImageUrls());
  471. assertEquals("4821", result.getDeliveryPinCode());
  472. assertEquals("OVER_5_TO_10_KG", result.getWeightRange());
  473. verify(fixture.logMapper, never()).selectList(any());
  474. }
  475. @Test
  476. void matchedReceiverCanViewAndConfirmButCannotCancelOrder() {
  477. Fixture fixture = new Fixture();
  478. FlashDeliveryOrder order = order(10L, "DELIVERED");
  479. order.setReceiverUserId(88L);
  480. when(fixture.orderMapper.selectById(10L)).thenReturn(order);
  481. when(fixture.orderMapper.transitionByParticipant(eq(10L), eq(88L), eq("DELIVERED"),
  482. eq("COMPLETED"), any(Date.class))).thenReturn(1);
  483. when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
  484. assertDoesNotThrow(() -> fixture.service.userDetail(88L, 10L));
  485. assertDoesNotThrow(() -> fixture.service.confirmReceipt(88L, 10L));
  486. assertThrows(ServiceException.class,
  487. () -> fixture.service.userCancel(88L, 10L, new com.ruoyi.app.flashdelivery.dto.FlashDeliveryReasonRequest()));
  488. verify(fixture.orderMapper).transitionByParticipant(eq(10L), eq(88L), eq("DELIVERED"),
  489. eq("COMPLETED"), any(Date.class));
  490. }
  491. @Test
  492. void orderWithoutCreationTimeReceiverBindingIsNotClaimedLater() {
  493. Fixture fixture = new Fixture();
  494. FlashDeliveryOrder historicalOrder = order(10L, "DELIVERED");
  495. historicalOrder.setReceiverUserId(null);
  496. when(fixture.orderMapper.selectById(10L)).thenReturn(historicalOrder);
  497. assertThrows(ServiceException.class, () -> fixture.service.userDetail(88L, 10L));
  498. verify(fixture.userMapper, never()).selectOrdinaryUserIdsByNormalizedPhone(any());
  499. }
  500. @Test
  501. void pickupRejectsMissingProofBeforeChangingState() {
  502. Fixture fixture = new Fixture();
  503. InfoUser rider = new InfoUser();
  504. rider.setUserId(8L);
  505. rider.setUserType("2");
  506. when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
  507. FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
  508. request.setImageUrls(List.of());
  509. assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
  510. verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
  511. }
  512. @Test
  513. void atomicAcceptFailureDoesNotBindSecondRider() {
  514. Fixture fixture = new Fixture();
  515. InfoUser rider = new InfoUser();
  516. rider.setUserId(8L);
  517. rider.setUserType("2");
  518. rider.setDeliveryType("FLASH");
  519. when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
  520. when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
  521. when(fixture.orderMapper.accept(eq(10L), eq(8L), any())).thenReturn(0);
  522. assertThrows(RuntimeException.class, () -> fixture.service.accept(8L, 10L));
  523. }
  524. @Test
  525. void flashAcceptUsesRiderLockAndChecksExclusivityBeforeAtomicUpdate() {
  526. Fixture fixture = new Fixture();
  527. rider(fixture, 8L);
  528. FlashDeliveryOrder waiting = order(10L, "WAITING_ACCEPTANCE");
  529. FlashDeliveryOrder accepted = order(10L, ACCEPTED);
  530. accepted.setRiderId(8L);
  531. when(fixture.orderMapper.selectById(10L)).thenReturn(waiting, accepted);
  532. when(fixture.orderMapper.accept(eq(10L), eq(8L), any(Date.class))).thenReturn(1);
  533. fixture.service.accept(8L, 10L);
  534. InOrder order = inOrder(fixture.lockService, fixture.exclusivityService, fixture.orderMapper);
  535. order.verify(fixture.lockService).withLock(eq(8L), any(RiderDeliveryLockService.LockedCall.class));
  536. order.verify(fixture.exclusivityService).assertCanAcceptFlash(8L, "NORMAL");
  537. order.verify(fixture.orderMapper).accept(eq(10L), eq(8L), any(Date.class));
  538. }
  539. @Test
  540. void flashExclusiveConflictDoesNotUpdateOrder() {
  541. Fixture fixture = new Fixture();
  542. rider(fixture, 8L);
  543. when(fixture.orderMapper.selectById(10L)).thenReturn(order(10L, "WAITING_ACCEPTANCE"));
  544. doThrow(new ServiceException("exclusive"))
  545. .when(fixture.exclusivityService).assertCanAcceptFlash(8L, "NORMAL");
  546. assertThrows(ServiceException.class, () -> fixture.service.accept(8L, 10L));
  547. verify(fixture.orderMapper, never()).accept(any(), any(), any());
  548. }
  549. @Test
  550. void oversizedProofUrlIsRejectedBeforeStateTransition() {
  551. Fixture fixture = new Fixture();
  552. InfoUser rider = new InfoUser();
  553. rider.setUserId(8L);
  554. rider.setUserType("2");
  555. when(fixture.userMapper.selectInfoUserByUserId(8L)).thenReturn(rider);
  556. FlashDeliveryProofRequest request = new FlashDeliveryProofRequest();
  557. request.setImageUrls(List.of("https://example.com/" + "a".repeat(1000)));
  558. assertThrows(RuntimeException.class, () -> fixture.service.pickup(8L, 10L, request));
  559. verify(fixture.orderMapper, never()).transitionByRider(any(), any(), any(), any(), any(), any());
  560. }
  561. @Test
  562. void missingServiceTypeReturnsBusinessErrorInsteadOfNullPointer() {
  563. Fixture fixture = new Fixture();
  564. FlashDeliveryQuoteRequest request = request();
  565. request.setServiceType(null);
  566. assertThrows(ServiceException.class, () -> fixture.service.quote(request));
  567. }
  568. @Test
  569. void equalTextAddressesAreRejectedEvenWhenCoordinatesDiffer() {
  570. Fixture fixture = new Fixture();
  571. FlashDeliveryQuoteRequest request = request();
  572. request.getDelivery().setAddress(request.getPickup().getAddress());
  573. request.getDelivery().setAddressDetail(request.getPickup().getAddressDetail());
  574. assertThrows(ServiceException.class, () -> fixture.service.quote(request));
  575. }
  576. @Test
  577. void concurrentPricingUpdateIsRejected() {
  578. Fixture fixture = new Fixture();
  579. when(fixture.pricingMapper.selectById(1L)).thenReturn(pricing());
  580. when(fixture.pricingMapper.countOverlapping("00:00", "12:00", 1L)).thenReturn(0);
  581. when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
  582. FlashDeliveryPricingRequest request = pricingRequest();
  583. request.setEndTime("12:00");
  584. request.setConfigVersion(1);
  585. assertThrows(ServiceException.class,
  586. () -> fixture.service.updatePricing(1L, 1L, request));
  587. verify(fixture.pricingMapper, never()).updateById(any(FlashDeliveryPricing.class));
  588. }
  589. @Test
  590. void overlappingPricingPeriodIsRejectedBeforeInsert() {
  591. Fixture fixture = new Fixture();
  592. FlashDeliveryPricingRequest request = pricingRequest();
  593. when(fixture.pricingMapper.countOverlapping("00:00", "24:00", null)).thenReturn(1);
  594. assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
  595. request.setDistance(new BigDecimal("0.004"));
  596. assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
  597. verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
  598. }
  599. @Test
  600. void pricingDistanceHalfCentimetreNormalizesToDatabaseMinimum() {
  601. Fixture fixture = new Fixture();
  602. FlashDeliveryPricingRequest request = pricingRequest();
  603. request.setDistance(new BigDecimal("0.005"));
  604. fixture.service.createPricing(1L, request);
  605. ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
  606. verify(fixture.pricingMapper).insert(saved.capture());
  607. assertEquals(new BigDecimal("0.01"), saved.getValue().getDistance());
  608. }
  609. @Test
  610. void pricingDistanceThatRoundsToZeroIsRejectedBeforePersistence() {
  611. Fixture fixture = new Fixture();
  612. FlashDeliveryPricingRequest request = pricingRequest();
  613. request.setDistance(new BigDecimal("0.001"));
  614. assertThrows(ServiceException.class, () -> fixture.service.createPricing(1L, request));
  615. verify(fixture.pricingMapper, never()).insert(any(FlashDeliveryPricing.class));
  616. }
  617. @Test
  618. void createPricingPersistsImmediatelyActiveTimePeriod() {
  619. Fixture fixture = new Fixture();
  620. FlashDeliveryPricingRequest request = pricingRequest();
  621. when(fixture.pricingMapper.countOverlapping("00:00", "24:00", null)).thenReturn(0);
  622. fixture.service.createPricing(7L, request);
  623. ArgumentCaptor<FlashDeliveryPricing> saved = ArgumentCaptor.forClass(FlashDeliveryPricing.class);
  624. verify(fixture.pricingMapper).insert(saved.capture());
  625. assertEquals(1, saved.getValue().getConfigVersion());
  626. assertEquals(7L, saved.getValue().getUpdatedBy());
  627. assertEquals(90L, saved.getValue().getStartingFare());
  628. InOrder writeOrder = inOrder(fixture.pricingMapper);
  629. writeOrder.verify(fixture.pricingMapper)
  630. .countOverlapping("00:00", "24:00", null);
  631. writeOrder.verify(fixture.pricingMapper).insert(any(FlashDeliveryPricing.class));
  632. }
  633. @Test
  634. void homeOmitsServiceWithoutCurrentPricingPeriod() {
  635. Fixture fixture = new Fixture();
  636. assertTrue(fixture.service.home().getServices().isEmpty());
  637. }
  638. @Test
  639. void quoteWithoutCurrentPricingPeriodFailsBeforeRouteLookup() {
  640. Fixture fixture = new Fixture();
  641. assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
  642. verify(fixture.routeService, never()).calculate(any(), any());
  643. }
  644. @Test
  645. void multipleCurrentPricingPeriodsFailExplicitlyBeforeRouteLookup() {
  646. Fixture fixture = new Fixture();
  647. when(fixture.pricingMapper.selectAtTime(anyString()))
  648. .thenReturn(List.of(pricing(), pricing()));
  649. assertThrows(ServiceException.class, () -> fixture.service.quote(request()));
  650. verify(fixture.routeService, never()).calculate(any(), any());
  651. }
  652. @Test
  653. void updatingPricingChecksOverlapBeforeVersionedWrite() {
  654. Fixture fixture = new Fixture();
  655. FlashDeliveryPricing existing = pricing();
  656. when(fixture.pricingMapper.selectById(1L)).thenReturn(existing);
  657. when(fixture.pricingMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
  658. FlashDeliveryPricingRequest request = pricingRequest();
  659. request.setConfigVersion(1);
  660. fixture.service.updatePricing(9L, 1L, request);
  661. InOrder writeOrder = inOrder(fixture.pricingMapper);
  662. writeOrder.verify(fixture.pricingMapper)
  663. .countOverlapping("00:00", "24:00", 1L);
  664. writeOrder.verify(fixture.pricingMapper).update(isNull(), any(Wrapper.class));
  665. }
  666. @Test
  667. void deletePricingRemovesConfigurationImmediately() {
  668. Fixture fixture = new Fixture();
  669. when(fixture.pricingMapper.selectById(18L)).thenReturn(pricing());
  670. when(fixture.pricingMapper.deleteById(18L)).thenReturn(1);
  671. fixture.service.deletePricing(18L);
  672. verify(fixture.pricingMapper).deleteById(18L);
  673. }
  674. @Test
  675. void urgentQuoteReturnsSeparatedFeesAndUsesScheduledPickupTime() {
  676. Fixture fixture = new Fixture();
  677. when(fixture.pricingMapper.selectAtTime("16:30")).thenReturn(List.of(pricing()));
  678. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(6000, 900, "ROUTE"));
  679. FlashDeliveryQuoteRequest request = request();
  680. request.setDeliveryType("URGENT");
  681. request.setTipAmount(5L);
  682. request.setDeliveryMode("SCHEDULED");
  683. Calendar calendar = Calendar.getInstance();
  684. calendar.add(Calendar.DAY_OF_MONTH, 1);
  685. calendar.set(Calendar.HOUR_OF_DAY, 16);
  686. calendar.set(Calendar.MINUTE, 30);
  687. calendar.set(Calendar.SECOND, 0);
  688. calendar.set(Calendar.MILLISECOND, 0);
  689. request.setScheduledPickupStartAt(calendar.getTime());
  690. request.setScheduledPickupEndAt(new Date(calendar.getTimeInMillis() + 30L * 60L * 1000L));
  691. var quote = fixture.service.quote(request);
  692. assertEquals(45L, quote.getDistanceFee());
  693. assertEquals(135L, quote.getBaseDeliveryFee());
  694. assertEquals(27L, quote.getUrgentFee());
  695. assertEquals(5L, quote.getTipAmount());
  696. assertEquals(167L, quote.getAmount());
  697. verify(fixture.pricingMapper).selectAtTime("16:30");
  698. }
  699. @Test
  700. void createRejectsChangedQuoteAndReturnsLatestQuoteWithoutInsert() {
  701. Fixture fixture = new Fixture();
  702. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  703. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
  704. FlashDeliveryCreateRequest request = createRequest();
  705. request.setQuotedAmount(91L);
  706. FlashDeliveryQuoteChangedException error = assertThrows(FlashDeliveryQuoteChangedException.class,
  707. () -> fixture.service.create(7L, request));
  708. assertEquals(90L, error.getLatestQuote().getAmount());
  709. assertEquals(1L, error.getLatestQuote().getPricingId());
  710. verify(fixture.orderMapper, never()).insert(any(FlashDeliveryOrder.class));
  711. }
  712. @Test
  713. void createDefaultsPayTypeToCashAndStoresTransferSelection() {
  714. Fixture fixture = new Fixture();
  715. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  716. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
  717. doAnswer(invocation -> {
  718. FlashDeliveryOrder order = invocation.getArgument(0);
  719. order.setId(99L);
  720. return 1;
  721. }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
  722. when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
  723. when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
  724. var defaultResult = fixture.service.create(7L, createRequest());
  725. FlashDeliveryCreateRequest transfer = createRequest();
  726. transfer.setPayType("6");
  727. var transferResult = fixture.service.create(7L, transfer);
  728. ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
  729. verify(fixture.orderMapper, times(2)).insert(saved.capture());
  730. assertEquals("4", saved.getAllValues().get(0).getPayType());
  731. assertEquals("6", saved.getAllValues().get(1).getPayType());
  732. assertEquals("4", defaultResult.getPayType());
  733. assertEquals("6", transferResult.getPayType());
  734. }
  735. @Test
  736. void createRejectsUnsupportedPayType() {
  737. Fixture fixture = new Fixture();
  738. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  739. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
  740. FlashDeliveryCreateRequest request = createRequest();
  741. request.setPayType("2");
  742. assertThrows(ServiceException.class, () -> fixture.service.create(7L, request));
  743. verify(fixture.orderMapper, never()).insert(any(FlashDeliveryOrder.class));
  744. }
  745. @Test
  746. void quoteValidatesDeliveryLevelAndCompleteItemInformation() {
  747. Fixture fixture = new Fixture();
  748. FlashDeliveryQuoteRequest request = request();
  749. request.setDeliveryType("FAST");
  750. FlashDeliveryQuoteRequest invalidDeliveryType = request;
  751. assertThrows(ServiceException.class, () -> fixture.service.quote(invalidDeliveryType));
  752. request = request();
  753. request.setQuantity(0);
  754. FlashDeliveryQuoteRequest invalidQuantity = request;
  755. assertThrows(ServiceException.class, () -> fixture.service.quote(invalidQuantity));
  756. request = request();
  757. request.setWeightRange("SMALL");
  758. FlashDeliveryQuoteRequest oldWeightRange = request;
  759. assertThrows(ServiceException.class, () -> fixture.service.quote(oldWeightRange));
  760. }
  761. @Test
  762. void quoteAcceptsAllFourWeightRanges() {
  763. Fixture fixture = new Fixture();
  764. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  765. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
  766. for (String weightRange : List.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
  767. "OVER_10_TO_15_KG", "OVER_15_TO_20_KG")) {
  768. FlashDeliveryQuoteRequest request = request();
  769. request.setWeightRange(weightRange);
  770. var quote = assertDoesNotThrow(() -> fixture.service.quote(request), weightRange);
  771. assertEquals(90L, quote.getAmount(), weightRange);
  772. }
  773. }
  774. @Test
  775. void createPersistsAndReturnsEveryWeightRange() {
  776. for (String weightRange : List.of("UP_TO_5_KG", "OVER_5_TO_10_KG",
  777. "OVER_10_TO_15_KG", "OVER_15_TO_20_KG")) {
  778. Fixture fixture = new Fixture();
  779. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  780. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(3200, 600, "ROUTE"));
  781. doAnswer(invocation -> {
  782. FlashDeliveryOrder order = invocation.getArgument(0);
  783. order.setId(99L);
  784. return 1;
  785. }).when(fixture.orderMapper).insert(any(FlashDeliveryOrder.class));
  786. when(fixture.imageMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
  787. when(fixture.logMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
  788. FlashDeliveryCreateRequest request = createRequest();
  789. request.setWeightRange(weightRange);
  790. var result = fixture.service.create(7L, request);
  791. ArgumentCaptor<FlashDeliveryOrder> saved = ArgumentCaptor.forClass(FlashDeliveryOrder.class);
  792. verify(fixture.orderMapper).insert(saved.capture());
  793. assertEquals(weightRange, saved.getValue().getWeightRange());
  794. assertEquals(weightRange, result.getWeightRange());
  795. }
  796. }
  797. @Test
  798. void quoteRejectsMissingAndUnknownWeightRanges() {
  799. Fixture fixture = new Fixture();
  800. for (String weightRange : new String[]{null, "", "SMALL", "OVER_20_KG"}) {
  801. FlashDeliveryQuoteRequest request = request();
  802. request.setWeightRange(weightRange);
  803. assertThrows(ServiceException.class, () -> fixture.service.quote(request), String.valueOf(weightRange));
  804. }
  805. }
  806. private FlashDeliveryQuoteRequest request() {
  807. FlashDeliveryQuoteRequest request = new FlashDeliveryQuoteRequest();
  808. request.setServiceType("HELP_SEND");
  809. request.setDeliveryType("NORMAL");
  810. request.setPackageType("DOCUMENT");
  811. request.setQuantity(1);
  812. request.setWeightRange("UP_TO_5_KG");
  813. request.setSpecification("30 x 20 x 10 cm");
  814. request.setTipAmount(0L);
  815. request.setDeliveryMode("NOW");
  816. request.setPickup(address("A", "0911111111", "25.0330", "121.5645"));
  817. request.setDelivery(address("B", "0922222222", "25.0478", "121.5319"));
  818. request.getDelivery().setAddress("second road");
  819. return request;
  820. }
  821. private FlashDeliveryCreateRequest createRequest() {
  822. FlashDeliveryCreateRequest request = new FlashDeliveryCreateRequest();
  823. FlashDeliveryQuoteRequest quote = request();
  824. request.setServiceType(quote.getServiceType());
  825. request.setDeliveryType(quote.getDeliveryType());
  826. request.setPackageType(quote.getPackageType());
  827. request.setQuantity(quote.getQuantity());
  828. request.setWeightRange(quote.getWeightRange());
  829. request.setSpecification(quote.getSpecification());
  830. request.setTipAmount(quote.getTipAmount());
  831. request.setPickup(quote.getPickup());
  832. request.setDelivery(quote.getDelivery());
  833. request.setClientRequestId("flash-scheduled-001");
  834. request.setDeliveryMode("SCHEDULED");
  835. Date start = new Date(System.currentTimeMillis() + 60L * 60L * 1000L);
  836. request.setScheduledPickupStartAt(start);
  837. request.setScheduledPickupEndAt(new Date(start.getTime() + 30L * 60L * 1000L));
  838. request.setPinRequired(true);
  839. request.setPricingId(1L);
  840. request.setPricingVersion(1);
  841. request.setQuotedBaseDeliveryFee(90L);
  842. request.setQuotedDistanceFee(0L);
  843. request.setQuotedUrgentFee(0L);
  844. request.setQuotedAmount(90L);
  845. request.setSenderImageUrls(List.of("https://example.com/sender.jpg"));
  846. return request;
  847. }
  848. private FlashDeliveryAddressRequest address(String name, String phone, String lat, String lon) {
  849. FlashDeliveryAddressRequest address = new FlashDeliveryAddressRequest();
  850. address.setName(name);
  851. address.setPhone(phone);
  852. address.setAddress("road");
  853. address.setAddressDetail("door");
  854. address.setLatitude(new BigDecimal(lat));
  855. address.setLongitude(new BigDecimal(lon));
  856. return address;
  857. }
  858. private FlashDeliveryPricing pricing() {
  859. FlashDeliveryPricing pricing = new FlashDeliveryPricing();
  860. pricing.setId(1L);
  861. pricing.setStartTime("00:00");
  862. pricing.setEndTime("24:00");
  863. pricing.setStartingDistance(new BigDecimal("3.00"));
  864. pricing.setStartingFare(90L);
  865. pricing.setDistance(new BigDecimal("1.00"));
  866. pricing.setFreight(15L);
  867. pricing.setUrgentRate(new BigDecimal("20.00"));
  868. pricing.setMinimumUrgentFee(10L);
  869. pricing.setConfigVersion(1);
  870. return pricing;
  871. }
  872. private FlashDeliveryPricingRequest pricingRequest() {
  873. FlashDeliveryPricingRequest request = new FlashDeliveryPricingRequest();
  874. request.setStartTime("00:00");
  875. request.setEndTime("24:00");
  876. request.setStartingDistance(new BigDecimal("3.00"));
  877. request.setStartingFare(90L);
  878. request.setDistance(new BigDecimal("1.00"));
  879. request.setFreight(15L);
  880. request.setUrgentRate(new BigDecimal("20.00"));
  881. request.setMinimumUrgentFee(10L);
  882. return request;
  883. }
  884. @SuppressWarnings({"rawtypes", "unchecked"})
  885. private void assertRiderTab(String tab, List<String> statuses) {
  886. Fixture fixture = new Fixture();
  887. rider(fixture, 8L);
  888. when(fixture.orderMapper.selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class),
  889. any(Wrapper.class))).thenReturn(new Page<FlashDeliveryOrder>());
  890. fixture.service.riderOrders(8L, 1, 10, tab, null, null);
  891. ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.query.QueryWrapper> query =
  892. ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper.class);
  893. verify(fixture.orderMapper).selectPage(any(com.baomidou.mybatisplus.core.metadata.IPage.class), query.capture());
  894. // MyBatis-Plus 的条件参数在渲染 SQL 段时才写入参数表,必须先取 SQL 再断言参数。
  895. String sql = query.getValue().getSqlSegment();
  896. assertTrue(sql.contains("rider_id"), tab);
  897. var parameters = query.getValue().getParamNameValuePairs().values();
  898. assertTrue(parameters.contains(8L), tab);
  899. for (String status : statuses) assertTrue(parameters.contains(status), tab + ":" + status);
  900. }
  901. private void rider(Fixture fixture, Long riderId) {
  902. InfoUser rider = new InfoUser();
  903. rider.setUserId(riderId);
  904. rider.setUserType("2");
  905. rider.setDeliveryType("FLASH");
  906. when(fixture.userMapper.selectInfoUserByUserId(riderId)).thenReturn(rider);
  907. }
  908. private FlashDeliveryOrder order(Long id, String status) {
  909. FlashDeliveryOrder order = new FlashDeliveryOrder();
  910. order.setId(id);
  911. order.setOrderNo("FL-20260813-021");
  912. order.setUserId(7L);
  913. order.setServiceType("HELP_SEND");
  914. order.setDeliveryType("NORMAL");
  915. order.setStatus(status);
  916. order.setPackageType("DOCUMENT");
  917. order.setQuantity(1);
  918. order.setWeightRange("UP_TO_5_KG");
  919. order.setDeliveryMode("NOW");
  920. order.setPinRequired(true);
  921. order.setDeliveryPinCode("4821");
  922. order.setPickupName("pickup contact");
  923. order.setPickupPhone("0911111111");
  924. order.setPickupAddress("pickup road");
  925. order.setPickupAddressDetail("pickup door");
  926. order.setPickupCity("台北市");
  927. order.setPickupArea("中山區");
  928. order.setPickupLongitude(new BigDecimal("121.5100"));
  929. order.setPickupLatitude(new BigDecimal("25.0100"));
  930. order.setDeliveryName("delivery contact");
  931. order.setDeliveryPhone("0922222222");
  932. order.setDeliveryAddress("delivery road");
  933. order.setDeliveryAddressDetail("delivery door");
  934. order.setDeliveryCity("台北市");
  935. order.setDeliveryArea("大安區");
  936. order.setDeliveryLongitude(new BigDecimal("121.5200"));
  937. order.setDeliveryLatitude(new BigDecimal("25.0200"));
  938. order.setDistanceMeters(3200);
  939. order.setEstimatedDurationSeconds(2100);
  940. order.setDistanceFee(12L);
  941. order.setBaseDeliveryFee(102L);
  942. order.setUrgentRate(new BigDecimal("20.00"));
  943. order.setMinimumUrgentFee(10L);
  944. order.setUrgentFee(0L);
  945. order.setTipAmount(10L);
  946. order.setAmount(112L);
  947. order.setCurrency("TWD");
  948. order.setUserNote("private note");
  949. order.setCreateTime(new Date());
  950. return order;
  951. }
  952. private FlashDeliveryOrderImage image(String proofType, String url) {
  953. FlashDeliveryOrderImage image = new FlashDeliveryOrderImage();
  954. image.setProofType(proofType);
  955. image.setImageUrl(url);
  956. return image;
  957. }
  958. @Test
  959. void missingOrInvalidBodyOrderIdIsRejectedBeforeDatabaseAccess() {
  960. Fixture fixture = new Fixture();
  961. for (Long orderId : java.util.Arrays.asList(null, 0L, -1L)) {
  962. ServiceException addressFailure = assertThrows(ServiceException.class,
  963. () -> fixture.service.updateAddress(7L, orderId, new FlashDeliveryAddressConfirmRequest()));
  964. ServiceException tipFailure = assertThrows(ServiceException.class,
  965. () -> fixture.service.addTip(7L, orderId, tipRequest(20L, 2)));
  966. assertEquals("flash.delivery.order.not.found", addressFailure.getMessage());
  967. assertEquals("flash.delivery.order.not.found", tipFailure.getMessage());
  968. }
  969. org.mockito.Mockito.verifyNoInteractions(fixture.orderMapper);
  970. }
  971. @Test
  972. void addressQuoteUsesLatestPricingForSlotTimeAndDoesNotWrite() {
  973. Fixture fixture = new Fixture();
  974. FlashDeliveryOrder order = editableOrder(fixture);
  975. // 已到预约时间也允许待接单订单改地址;报价读取该时点的最新运价而非订单旧快照。
  976. order.setDeliveryMode("SCHEDULED");
  977. order.setScheduledPickupStartAt(new Date(1));
  978. order.setScheduledPickupEndAt(new Date(1800001));
  979. FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, addressChange());
  980. assertEquals(4500, quote.getDistanceMeters());
  981. assertEquals(900, quote.getEstimatedDurationSeconds());
  982. assertEquals(23L, quote.getDistanceFee());
  983. assertEquals(113L, quote.getBaseDeliveryFee());
  984. assertEquals(10L, quote.getTipAmount());
  985. assertEquals(123L, quote.getAmount());
  986. assertEquals(2, quote.getOrderVersion());
  987. assertEquals(1, quote.getPricingVersion());
  988. assertEquals("pickup road", order.getPickupAddress());
  989. // 改址报价必须查询运价表(按定价时点取最新),不再沿用订单快照
  990. verify(fixture.pricingMapper).selectAtTime(anyString());
  991. verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
  992. verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
  993. }
  994. @Test
  995. void addressSaveUpdatesBothEndsRouteFeesAndReceiverTogether() {
  996. Fixture fixture = new Fixture();
  997. FlashDeliveryOrder order = editableOrder(fixture);
  998. when(fixture.userMapper.selectOrdinaryUserIdsByNormalizedPhone("0922222222"))
  999. .thenReturn(List.of(88L));
  1000. when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
  1001. FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
  1002. input.getPickup().setName(" new sender ");
  1003. input.getPickup().setAddressDetail(null);
  1004. input.getDelivery().setName("new receiver");
  1005. var view = fixture.service.updateAddress(7L, 10L, input);
  1006. assertEquals("new sender", view.getPickup().getName());
  1007. assertEquals("new receiver", view.getDelivery().getName());
  1008. assertNull(view.getPickup().getAddressDetail());
  1009. assertEquals(4500, view.getDistanceMeters());
  1010. assertEquals(123L, view.getAmount());
  1011. assertEquals(3, view.getOrderVersion());
  1012. assertEquals(88L, order.getReceiverUserId());
  1013. assertEquals(10L, order.getTipAmount());
  1014. assertEquals("WAITING_ACCEPTANCE", order.getStatus());
  1015. verify(fixture.orderMapper).updateWaitingAddress(order, 2);
  1016. verify(fixture.logMapper).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
  1017. }
  1018. @Test
  1019. void addressChangeClearsFormerReceiverWhenNewPhoneDoesNotMatch() {
  1020. Fixture fixture = new Fixture();
  1021. FlashDeliveryOrder order = editableOrder(fixture);
  1022. order.setReceiverUserId(88L);
  1023. when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(1);
  1024. fixture.service.updateAddress(7L, 10L, confirmAddress(fixture));
  1025. assertNull(order.getReceiverUserId());
  1026. }
  1027. @Test
  1028. void addressQuotePreservesUrgentRulesAndExistingTip() {
  1029. Fixture fixture = new Fixture();
  1030. FlashDeliveryOrder order = editableOrder(fixture);
  1031. order.setDeliveryType("URGENT");
  1032. var quote = fixture.service.quoteAddress(7L, 10L, addressChange());
  1033. assertEquals(23L, quote.getUrgentFee());
  1034. assertEquals(146L, quote.getAmount());
  1035. assertEquals(10L, quote.getTipAmount());
  1036. }
  1037. @Test
  1038. void changedRouteOrUnconfirmedPriceReturnsLatestQuoteWithoutSaving() {
  1039. Fixture fixture = new Fixture();
  1040. editableOrder(fixture);
  1041. FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
  1042. input.setQuotedAmount(1L);
  1043. var exception = assertThrows(FlashDeliveryQuoteChangedException.class,
  1044. () -> fixture.service.updateAddress(7L, 10L, input));
  1045. assertEquals(123L, exception.getLatestQuote().getAmount());
  1046. input.setQuotedAmount(123L);
  1047. input.setQuotedDistanceMeters(4499);
  1048. assertThrows(FlashDeliveryQuoteChangedException.class,
  1049. () -> fixture.service.updateAddress(7L, 10L, input));
  1050. input.setQuotedDistanceMeters(4500);
  1051. input.setQuotedUrgentFee(null);
  1052. assertThrows(FlashDeliveryQuoteChangedException.class,
  1053. () -> fixture.service.updateAddress(7L, 10L, input));
  1054. verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
  1055. verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
  1056. }
  1057. @Test
  1058. void receiverAndOtherUsersCannotQuoteEditOrAddTip() {
  1059. Fixture fixture = new Fixture();
  1060. FlashDeliveryOrder order = editableOrder(fixture);
  1061. order.setReceiverUserId(88L);
  1062. for (Long user : List.of(88L, 99L)) {
  1063. assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(user, 10L, addressChange()));
  1064. assertThrows(ServiceException.class, () -> fixture.service.updateAddress(user, 10L, new FlashDeliveryAddressConfirmRequest()));
  1065. assertThrows(ServiceException.class, () -> fixture.service.addTip(user, 10L, tipRequest(5L, 2)));
  1066. }
  1067. verify(fixture.routeService, never()).calculate(any(), any());
  1068. verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
  1069. }
  1070. @Test
  1071. void everyNonWaitingStateAndAssignedRiderRejectsBothChanges() {
  1072. for (String status : List.of("ACCEPTED", "PICKED_UP", "DELIVERED", "COMPLETED", "CANCELLED")) {
  1073. Fixture fixture = new Fixture();
  1074. FlashDeliveryOrder order = editableOrder(fixture);
  1075. order.setStatus(status);
  1076. assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
  1077. assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, new FlashDeliveryAddressConfirmRequest()));
  1078. assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
  1079. }
  1080. Fixture fixture = new Fixture();
  1081. editableOrder(fixture).setRiderId(8L);
  1082. assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
  1083. assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(5L, 2)));
  1084. }
  1085. @Test
  1086. void staleOrMissingVersionCannotOverwriteNewerOrder() {
  1087. Fixture fixture = new Fixture();
  1088. editableOrder(fixture);
  1089. FlashDeliveryAddressChangeRequest input = addressChange();
  1090. input.setOrderVersion(1);
  1091. assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
  1092. for (Integer version : java.util.Arrays.asList(null, -1, 1)) {
  1093. assertThrows(ServiceException.class,
  1094. () -> fixture.service.addTip(7L, 10L, tipRequest(5L, version)));
  1095. }
  1096. verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
  1097. }
  1098. @Test
  1099. void addressValidationRejectsSameInvalidAndOverDistanceEndpoints() {
  1100. Fixture fixture = new Fixture();
  1101. editableOrder(fixture);
  1102. FlashDeliveryAddressChangeRequest input = addressChange();
  1103. input.setDelivery(input.getPickup());
  1104. assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
  1105. input.setDelivery(request().getDelivery());
  1106. input.getPickup().setPhone("");
  1107. assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, input));
  1108. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(40001, 1000, "GOOGLE"));
  1109. assertThrows(ServiceException.class, () -> fixture.service.quoteAddress(7L, 10L, addressChange()));
  1110. verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
  1111. }
  1112. @Test
  1113. void riderAcceptanceBetweenReadAndAddressSaveRejectsWriteWithoutLog() {
  1114. Fixture fixture = new Fixture();
  1115. editableOrder(fixture);
  1116. FlashDeliveryAddressConfirmRequest input = confirmAddress(fixture);
  1117. when(fixture.orderMapper.updateWaitingAddress(any(), eq(2))).thenReturn(0);
  1118. ServiceException exception = assertThrows(ServiceException.class,
  1119. () -> fixture.service.updateAddress(7L, 10L, input));
  1120. assertEquals("flash.delivery.state.changed", exception.getMessage());
  1121. verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
  1122. }
  1123. @Test
  1124. void addingTipOnlyIncreasesTipAndTotalAndRejectsSameVersionRetry() {
  1125. Fixture fixture = new Fixture();
  1126. FlashDeliveryOrder order = editableOrder(fixture);
  1127. when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
  1128. var view = fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
  1129. assertEquals(30L, view.getTipAmount());
  1130. assertEquals(132L, view.getAmount());
  1131. assertEquals(102L, view.getBaseDeliveryFee());
  1132. assertEquals(3200, view.getDistanceMeters());
  1133. assertEquals(3, view.getOrderVersion());
  1134. assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
  1135. assertEquals(30L, order.getTipAmount());
  1136. verify(fixture.orderMapper).updateWaitingTip(any(), eq(2));
  1137. verify(fixture.routeService, never()).calculate(any(), any());
  1138. verify(fixture.pricingMapper, never()).selectAtTime(anyString());
  1139. }
  1140. @Test
  1141. void tipChangeInvalidatesEarlierAddressConfirmation() {
  1142. Fixture fixture = new Fixture();
  1143. editableOrder(fixture);
  1144. FlashDeliveryAddressConfirmRequest confirmation = confirmAddress(fixture);
  1145. when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
  1146. fixture.service.addTip(7L, 10L, tipRequest(20L, 2));
  1147. assertThrows(ServiceException.class, () -> fixture.service.updateAddress(7L, 10L, confirmation));
  1148. verify(fixture.orderMapper, never()).updateWaitingAddress(any(), any());
  1149. }
  1150. @Test
  1151. void additionalTipRejectsFractionalJsonBeforeWriting() throws Exception {
  1152. Fixture fixture = new Fixture();
  1153. editableOrder(fixture);
  1154. when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(1);
  1155. FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
  1156. "{\"orderVersion\":2,\"additionalTipAmount\":1.9}", FlashDeliveryTipAddRequest.class);
  1157. ServiceException exception = assertThrows(ServiceException.class,
  1158. () -> fixture.service.addTip(7L, 10L, input));
  1159. assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
  1160. verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
  1161. }
  1162. @Test
  1163. void additionalTipRejectsJsonAmountBeyondLongRange() throws Exception {
  1164. Fixture fixture = new Fixture();
  1165. editableOrder(fixture);
  1166. FlashDeliveryTipAddRequest input = new com.fasterxml.jackson.databind.ObjectMapper().readValue(
  1167. "{\"orderVersion\":2,\"additionalTipAmount\":1e100}", FlashDeliveryTipAddRequest.class);
  1168. ServiceException exception = assertThrows(ServiceException.class,
  1169. () -> fixture.service.addTip(7L, 10L, input));
  1170. assertEquals("flash.delivery.tip.additional.invalid", exception.getMessage());
  1171. verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
  1172. }
  1173. @Test
  1174. void additionalTipRejectsZeroNegativeNullAndOverflow() {
  1175. Fixture fixture = new Fixture();
  1176. editableOrder(fixture);
  1177. for (Long value : java.util.Arrays.asList(null, 0L, -1L, Long.MAX_VALUE)) {
  1178. assertThrows(ServiceException.class,
  1179. () -> fixture.service.addTip(7L, 10L, tipRequest(value, 2)));
  1180. }
  1181. verify(fixture.orderMapper, never()).updateWaitingTip(any(), any());
  1182. }
  1183. @Test
  1184. void riderAcceptanceBetweenReadAndTipSaveRejectsWriteWithoutLog() {
  1185. Fixture fixture = new Fixture();
  1186. editableOrder(fixture);
  1187. when(fixture.orderMapper.updateWaitingTip(any(), eq(2))).thenReturn(0);
  1188. assertThrows(ServiceException.class, () -> fixture.service.addTip(7L, 10L, tipRequest(20L, 2)));
  1189. verify(fixture.logMapper, never()).insert(any(com.ruoyi.system.domain.flash.FlashDeliveryOrderLog.class));
  1190. }
  1191. private FlashDeliveryOrder editableOrder(Fixture fixture) {
  1192. FlashDeliveryOrder order = order(10L, "WAITING_ACCEPTANCE");
  1193. order.setRiderId(null);
  1194. order.setVersion(2);
  1195. order.setPricingId(1L);
  1196. order.setPricingVersion(1);
  1197. order.setPricingStartTime("00:00");
  1198. order.setPricingEndTime("24:00");
  1199. order.setStartingDistance(new BigDecimal("3.00"));
  1200. order.setStartingFare(90L);
  1201. order.setDistance(new BigDecimal("1.00"));
  1202. order.setFreight(15L);
  1203. when(fixture.orderMapper.selectById(10L)).thenReturn(order);
  1204. when(fixture.routeService.calculate(any(), any())).thenReturn(new RouteDistance(4500, 900, "GOOGLE"));
  1205. // 031 后改址按订单定价时点读最新运价:stub 与订单快照同值,金额断言维持不变
  1206. when(fixture.pricingMapper.selectAtTime(anyString())).thenReturn(List.of(pricing()));
  1207. return order;
  1208. }
  1209. private FlashDeliveryAddressChangeRequest addressChange() {
  1210. FlashDeliveryAddressChangeRequest input = new FlashDeliveryAddressChangeRequest();
  1211. input.setOrderVersion(2);
  1212. input.setPickup(request().getPickup());
  1213. input.setDelivery(request().getDelivery());
  1214. return input;
  1215. }
  1216. private FlashDeliveryAddressConfirmRequest confirmAddress(Fixture fixture) {
  1217. FlashDeliveryAddressChangeRequest change = addressChange();
  1218. FlashDeliveryQuoteView quote = fixture.service.quoteAddress(7L, 10L, change);
  1219. FlashDeliveryAddressConfirmRequest input = new FlashDeliveryAddressConfirmRequest();
  1220. input.setOrderVersion(quote.getOrderVersion());
  1221. input.setPickup(change.getPickup());
  1222. input.setDelivery(change.getDelivery());
  1223. input.setQuotedDistanceMeters(quote.getDistanceMeters());
  1224. input.setQuotedBaseDeliveryFee(quote.getBaseDeliveryFee());
  1225. input.setQuotedDistanceFee(quote.getDistanceFee());
  1226. input.setQuotedUrgentFee(quote.getUrgentFee());
  1227. input.setQuotedAmount(quote.getAmount());
  1228. return input;
  1229. }
  1230. private FlashDeliveryTipAddRequest tipRequest(Long amount, Integer version) {
  1231. FlashDeliveryTipAddRequest input = new FlashDeliveryTipAddRequest();
  1232. input.setAdditionalTipAmount(amount == null ? null : BigDecimal.valueOf(amount));
  1233. input.setOrderVersion(version);
  1234. return input;
  1235. }
  1236. private static class Fixture {
  1237. final FlashDeliveryOrderMapper orderMapper = mock(FlashDeliveryOrderMapper.class);
  1238. final FlashDeliveryPricingMapper pricingMapper = mock(FlashDeliveryPricingMapper.class);
  1239. final FlashDeliveryOrderImageMapper imageMapper = mock(FlashDeliveryOrderImageMapper.class);
  1240. final FlashDeliveryOrderLogMapper logMapper = mock(FlashDeliveryOrderLogMapper.class);
  1241. final InfoUserMapper userMapper = mock(InfoUserMapper.class);
  1242. final FlashDeliveryRouteService routeService = mock(FlashDeliveryRouteService.class);
  1243. final RiderDeliveryLockService lockService = mock(RiderDeliveryLockService.class);
  1244. final RiderDeliveryExclusivityService exclusivityService = mock(RiderDeliveryExclusivityService.class);
  1245. final FlashDeliveryApplicationService service = new FlashDeliveryApplicationService(
  1246. orderMapper, pricingMapper, imageMapper, logMapper, userMapper,
  1247. routeService, new FlashDeliveryPricingCalculator(), lockService, exclusivityService);
  1248. Fixture() {
  1249. when(lockService.withLock(any(), any())).thenAnswer(invocation -> {
  1250. RiderDeliveryLockService.LockedCall<?> call = invocation.getArgument(1);
  1251. return call.call();
  1252. });
  1253. }
  1254. }
  1255. }