FlashDeliveryNotificationServiceTest.java 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. package com.ruoyi.app.flashdelivery.service;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.JSONObject;
  4. import com.ruoyi.app.push.ApnsPushSender;
  5. import com.ruoyi.app.push.BusinessPushService;
  6. import com.ruoyi.app.push.FcmPushSender;
  7. import com.ruoyi.app.utils.PayPush;
  8. import com.ruoyi.app.utils.event.PushEvent;
  9. import com.ruoyi.app.utils.event.PushEventListener;
  10. import com.ruoyi.common.utils.LocaleUtils;
  11. import com.ruoyi.common.utils.spring.SpringUtils;
  12. import com.ruoyi.framework.manager.AsyncManager;
  13. import com.ruoyi.system.domain.PushMessage;
  14. import com.ruoyi.system.domain.flash.FlashDeliveryOrderStop;
  15. import com.ruoyi.system.service.IInfoUserService;
  16. import com.ruoyi.system.service.IPushMessageService;
  17. import com.ruoyi.app.utils.event.PushEventService;
  18. import com.ruoyi.app.order.dto.OrderPushBodyDto;
  19. import com.ruoyi.app.push.BusinessPushProperties;
  20. import com.ruoyi.app.push.NativeRiderBatchPushService;
  21. import com.ruoyi.app.push.UserNotificationScene;
  22. import com.ruoyi.system.domain.InfoUser;
  23. import com.ruoyi.system.domain.RiderPosition;
  24. import com.ruoyi.common.core.domain.entity.SysDictData;
  25. import com.ruoyi.system.domain.flash.FlashDeliveryOrder;
  26. import com.ruoyi.system.mapper.InfoUserMapper;
  27. import com.ruoyi.system.mapper.RiderPositionMapper;
  28. import com.ruoyi.common.utils.DictUtils;
  29. import org.junit.jupiter.api.Test;
  30. import org.mockito.MockedStatic;
  31. import org.springframework.context.ApplicationEventPublisher;
  32. import org.springframework.context.MessageSource;
  33. import org.springframework.context.support.ResourceBundleMessageSource;
  34. import org.springframework.test.util.ReflectionTestUtils;
  35. import org.springframework.transaction.support.TransactionSynchronization;
  36. import org.springframework.transaction.support.TransactionSynchronizationManager;
  37. import java.math.BigDecimal;
  38. import java.util.List;
  39. import java.util.ArrayList;
  40. import java.util.Locale;
  41. import java.util.Set;
  42. import java.util.TimerTask;
  43. import java.util.concurrent.ScheduledExecutorService;
  44. import static org.mockito.ArgumentMatchers.any;
  45. import static org.mockito.ArgumentMatchers.anyString;
  46. import static org.mockito.ArgumentMatchers.eq;
  47. import static org.mockito.ArgumentMatchers.same;
  48. import static org.mockito.Mockito.doNothing;
  49. import static org.mockito.Mockito.mock;
  50. import static org.mockito.Mockito.never;
  51. import static org.mockito.Mockito.spy;
  52. import static org.mockito.Mockito.timeout;
  53. import static org.mockito.Mockito.verify;
  54. import static org.mockito.Mockito.when;
  55. import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
  56. import static org.junit.jupiter.api.Assertions.assertEquals;
  57. import static org.junit.jupiter.api.Assertions.assertFalse;
  58. import static org.junit.jupiter.api.Assertions.assertTrue;
  59. import static org.mockito.Mockito.doAnswer;
  60. import static org.mockito.Mockito.doThrow;
  61. import static org.mockito.Mockito.mockStatic;
  62. class FlashDeliveryNotificationServiceTest {
  63. @Test
  64. void successfulUserNotificationsCarryScenesWithoutDeviceAddressAfterCommit() {
  65. InfoUserMapper users = mock(InfoUserMapper.class);
  66. InfoUser sender = new InfoUser(); sender.setUserId(7L);
  67. when(users.selectById(7L)).thenReturn(sender);
  68. FlashDeliveryNotificationService service = new FlashDeliveryNotificationService(users, mock(PushEventService.class));
  69. java.util.List<com.alibaba.fastjson.JSONObject> bodies = new java.util.ArrayList<>();
  70. try (var spring = org.mockito.Mockito.mockStatic(com.ruoyi.common.utils.spring.SpringUtils.class)) {
  71. spring.when(() -> com.ruoyi.common.utils.spring.SpringUtils.getBean("scheduledExecutorService"))
  72. .thenReturn(mock(java.util.concurrent.ScheduledExecutorService.class));
  73. com.ruoyi.framework.manager.AsyncManager manager = mock(com.ruoyi.framework.manager.AsyncManager.class);
  74. org.mockito.Mockito.doAnswer(call -> { ((java.util.TimerTask) call.getArgument(0)).run(); return null; })
  75. .when(manager).execute(any(java.util.TimerTask.class));
  76. try (var async = org.mockito.Mockito.mockStatic(com.ruoyi.framework.manager.AsyncManager.class);
  77. var push = org.mockito.Mockito.mockStatic(com.ruoyi.app.utils.PayPush.class)) {
  78. async.when(com.ruoyi.framework.manager.AsyncManager::me).thenReturn(manager);
  79. push.when(() -> com.ruoyi.app.utils.PayPush.userPushHandleLocal(any(), any(), eq(7L),
  80. org.mockito.ArgumentMatchers.isNull(), anyString(), anyString(), anyString(), anyString(), anyString()))
  81. .thenAnswer(call -> { bodies.add(com.alibaba.fastjson.JSON.parseObject(call.getArgument(6, String.class))); return null; });
  82. org.springframework.transaction.support.TransactionSynchronizationManager.initSynchronization();
  83. try {
  84. service.notifyAccepted(order()); service.notifyPickedUp(order()); service.notifyDelivered(order());
  85. org.junit.jupiter.api.Assertions.assertTrue(bodies.isEmpty());
  86. org.springframework.transaction.support.TransactionSynchronizationManager.getSynchronizations()
  87. .forEach(sync -> sync.afterCommit());
  88. org.junit.jupiter.api.Assertions.assertEquals(List.of("RIDER_ACCEPTED", "ORDER_DELIVERING", "ORDER_DELIVERED"),
  89. bodies.stream().map(body -> body.getString("eventScene")).toList());
  90. bodies.forEach(body -> org.junit.jupiter.api.Assertions.assertEquals(3, body.getIntValue("pushType")));
  91. } finally {
  92. org.springframework.transaction.support.TransactionSynchronizationManager.clearSynchronization();
  93. }
  94. }
  95. }
  96. }
  97. @Test
  98. void rollbackSkipsLookupAndPreparationFailureDoesNotEscapeBusinessOperation() {
  99. InfoUserMapper users = mock(InfoUserMapper.class);
  100. FlashDeliveryNotificationService service = new FlashDeliveryNotificationService(users, mock(PushEventService.class));
  101. org.springframework.transaction.support.TransactionSynchronizationManager.initSynchronization();
  102. try {
  103. service.notifyAccepted(order());
  104. org.springframework.transaction.support.TransactionSynchronizationManager.getSynchronizations()
  105. .forEach(sync -> sync.afterCompletion(1));
  106. verify(users, never()).selectById(any());
  107. } finally {
  108. org.springframework.transaction.support.TransactionSynchronizationManager.clearSynchronization();
  109. }
  110. when(users.selectById(7L)).thenThrow(new IllegalStateException("push preparation failed"));
  111. org.junit.jupiter.api.Assertions.assertDoesNotThrow(() -> service.notifyAccepted(order()));
  112. }
  113. @Test
  114. void availableOrderRoutesNewFlashOrderToNearbyEligibleRiders() {
  115. InfoUserMapper userMapper = mock(InfoUserMapper.class);
  116. RiderPositionMapper positions = mock(RiderPositionMapper.class);
  117. FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService(
  118. userMapper, mock(PushEventService.class), positions));
  119. RiderPosition rider = new RiderPosition();
  120. rider.setRiderId(9L);
  121. rider.setCid("rider-cid");
  122. FlashDeliveryOrder order = order();
  123. order.setStatus("WAITING_ACCEPTANCE");
  124. order.setIsDisplay(true);
  125. order.setVehicleType(1);
  126. order.setDeliveryType("NORMAL");
  127. order.setPickupLongitude(new BigDecimal("121.5000"));
  128. order.setPickupLatitude(new BigDecimal("25.0300"));
  129. SysDictData distance = new SysDictData();
  130. distance.setDictValue("5");
  131. when(positions.getFlashRiderNotificationList(order.getPickupLongitude(), order.getPickupLatitude(),
  132. 1, "NORMAL", 5, 20)).thenReturn(List.of(rider));
  133. doNothing().when(service).sendAvailableOrderToRider(rider, order);
  134. try (MockedStatic<DictUtils> dictionary = org.mockito.Mockito.mockStatic(DictUtils.class)) {
  135. dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance"))
  136. .thenReturn(List.of(distance));
  137. service.notifyAvailable(order);
  138. }
  139. verify(service).sendAvailableOrderToRider(rider, order);
  140. }
  141. @Test
  142. void nativeAvailableFlashOrderUsesRiderBatchAndUniKeepsPerRiderPath() {
  143. InfoUserMapper userMapper = mock(InfoUserMapper.class);
  144. RiderPositionMapper positions = mock(RiderPositionMapper.class);
  145. BusinessPushProperties properties = new BusinessPushProperties();
  146. NativeRiderBatchPushService batches = mock(NativeRiderBatchPushService.class);
  147. FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService(
  148. userMapper, mock(PushEventService.class), positions, properties, batches));
  149. FlashDeliveryOrder order = order();
  150. order.setStatus("WAITING_ACCEPTANCE");
  151. order.setIsDisplay(true);
  152. order.setVehicleType(1);
  153. order.setDeliveryType("NORMAL");
  154. order.setPickupLongitude(new BigDecimal("121.5000"));
  155. order.setPickupLatitude(new BigDecimal("25.0300"));
  156. RiderPosition rider = new RiderPosition();
  157. rider.setRiderId(9L);
  158. List<RiderPosition> riders = List.of(rider);
  159. SysDictData distance = new SysDictData();
  160. distance.setDictValue("5");
  161. when(positions.getFlashRiderNotificationList(order.getPickupLongitude(), order.getPickupLatitude(),
  162. 1, "NORMAL", 5, 20)).thenReturn(riders);
  163. doNothing().when(service).sendAvailableOrderToRider(rider, order);
  164. try (MockedStatic<DictUtils> dictionary = org.mockito.Mockito.mockStatic(DictUtils.class)) {
  165. dictionary.when(() -> DictUtils.getDictCache("sys_qs_newtask_distance"))
  166. .thenReturn(List.of(distance));
  167. properties.setMode(BusinessPushProperties.Mode.NATIVE);
  168. service.notifyAvailable(order);
  169. verify(batches, timeout(3000)).sendNewOrder(org.mockito.ArgumentMatchers.eq(riders),
  170. org.mockito.ArgumentMatchers.eq("no.message.push.message"),
  171. org.mockito.ArgumentMatchers.eq("no.message.push.new.flash.order"),
  172. org.mockito.ArgumentMatchers.eq(OrderPushBodyDto.getJson(order.getOrderNo(),
  173. "WAITING_ACCEPTANCE", 0, 3)),
  174. org.mockito.ArgumentMatchers.eq(order.getOrderNo()), org.mockito.ArgumentMatchers.any());
  175. verify(service, never()).sendAvailableOrderToRider(rider, order);
  176. properties.setMode(BusinessPushProperties.Mode.UNI);
  177. service.notifyAvailable(order);
  178. verify(service).sendAvailableOrderToRider(rider, order);
  179. }
  180. }
  181. @Test
  182. void acceptedNotificationRoutesToSender() {
  183. InfoUserMapper userMapper = mock(InfoUserMapper.class);
  184. FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService(
  185. userMapper, mock(PushEventService.class)));
  186. doNothing().when(service).sendToUser(any(), any(), anyString(), anyString(), any());
  187. InfoUser sender = new InfoUser();
  188. sender.setUserId(7L);
  189. FlashDeliveryOrder order = order();
  190. when(userMapper.selectById(7L)).thenReturn(sender);
  191. service.notifyAccepted(order);
  192. verify(service).sendToUser(same(sender), same(order), eq("ACCEPTED"),
  193. eq("no.message.push.delivery.personnel.receiving.order"), eq(UserNotificationScene.RIDER_ACCEPTED));
  194. }
  195. @Test
  196. void missingSenderSkipsNotification() {
  197. InfoUserMapper userMapper = mock(InfoUserMapper.class);
  198. FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService(
  199. userMapper, mock(PushEventService.class)));
  200. when(userMapper.selectById(7L)).thenReturn(null);
  201. service.notifyAccepted(order());
  202. verify(service, never()).sendToUser(any(), any(), anyString(), anyString(), any());
  203. }
  204. @Test
  205. void pickedUpNotificationRoutesToSender() {
  206. InfoUserMapper userMapper = mock(InfoUserMapper.class);
  207. FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService(
  208. userMapper, mock(PushEventService.class)));
  209. doNothing().when(service).sendToUser(any(), any(), anyString(), anyString(), any());
  210. InfoUser sender = new InfoUser();
  211. sender.setUserId(7L);
  212. FlashDeliveryOrder order = order();
  213. when(userMapper.selectById(7L)).thenReturn(sender);
  214. service.notifyPickedUp(order);
  215. verify(service).sendToUser(same(sender), same(order), eq("PICKED_UP"),
  216. eq("no.message.push.delivery.personnel.qspsz.order"), eq(UserNotificationScene.ORDER_DELIVERING));
  217. }
  218. @Test
  219. void deliveredNotificationRoutesToSender() {
  220. InfoUserMapper userMapper = mock(InfoUserMapper.class);
  221. FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService(
  222. userMapper, mock(PushEventService.class)));
  223. doNothing().when(service).sendToUser(any(), any(), anyString(), anyString(), any());
  224. InfoUser sender = new InfoUser();
  225. sender.setUserId(7L);
  226. FlashDeliveryOrder order = order();
  227. when(userMapper.selectById(7L)).thenReturn(sender);
  228. service.notifyDelivered(order);
  229. verify(service).sendToUser(same(sender), same(order), eq("DELIVERED"),
  230. eq("no.message.push.delivery.personnel.qsysd.order"), eq(UserNotificationScene.ORDER_DELIVERED));
  231. }
  232. @Test
  233. void cancelledNotificationRoutesToAssignedRider() {
  234. InfoUserMapper userMapper = mock(InfoUserMapper.class);
  235. FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService(
  236. userMapper, mock(PushEventService.class)));
  237. doNothing().when(service).sendToRider(any(), any(), anyString(), anyString());
  238. InfoUser rider = new InfoUser();
  239. rider.setUserId(9L);
  240. FlashDeliveryOrder order = order();
  241. order.setRiderId(9L);
  242. when(userMapper.selectById(9L)).thenReturn(rider);
  243. service.notifyCancelled(order);
  244. verify(service).sendToRider(same(rider), same(order), eq("CANCELLED"),
  245. eq("no.message.push.order.cancelled"));
  246. }
  247. @Test
  248. void cancelledNotificationSkipsWhenNoRiderAssigned() {
  249. InfoUserMapper userMapper = mock(InfoUserMapper.class);
  250. FlashDeliveryNotificationService service = spy(new FlashDeliveryNotificationService(
  251. userMapper, mock(PushEventService.class)));
  252. service.notifyCancelled(order());
  253. verify(service, never()).sendToRider(any(), any(), anyString(), anyString());
  254. verify(service, never()).sendToUser(any(), any(), anyString(), anyString(), any());
  255. }
  256. @Test
  257. void pickupStopProgressIsQueuedOnlyAfterCommitAndSentOnlyToOrderSender() {
  258. try (StopNotificationFixture fixture = new StopNotificationFixture()) {
  259. FlashDeliveryOrder order = order();
  260. order.setStatus("ACCEPTED");
  261. order.setReceiverUserId(88L);
  262. order.setDeliveryPinCode("order-secret-pin");
  263. FlashDeliveryOrderStop stop = stop("PICKUP", 2);
  264. TransactionSynchronizationManager.initSynchronization();
  265. try {
  266. assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order, stop, 1, 2));
  267. assertTrue(fixture.tasks.isEmpty());
  268. assertTrue(fixture.messages.isEmpty());
  269. assertTrue(fixture.lookedUpUsers.isEmpty());
  270. TransactionSynchronizationManager.getSynchronizations().forEach(TransactionSynchronization::afterCommit);
  271. assertEquals(1, fixture.tasks.size());
  272. assertTrue(fixture.messages.isEmpty());
  273. assertEquals(List.of(7L), fixture.lookedUpUsers);
  274. fixture.runTasks();
  275. } finally {
  276. TransactionSynchronizationManager.clearSynchronization();
  277. }
  278. assertEquals(1, fixture.messages.size());
  279. PushMessage message = fixture.messages.get(0);
  280. assertEquals(7L, message.getUserId());
  281. assertEquals("FL-20260813-021", message.getDdId());
  282. assertEquals(3, message.getPushType());
  283. JSONObject body = JSON.parseObject(message.getBody());
  284. assertEquals(Set.of("ddId", "state", "type", "pushType", "eventType", "stopId", "stopType",
  285. "stopOrder", "completedStopCount", "totalStopCount"), body.keySet());
  286. assertEquals("FL-20260813-021", body.getString("ddId"));
  287. assertEquals("ACCEPTED", body.getString("state"));
  288. assertEquals(-1, body.getIntValue("type"));
  289. assertEquals(3, body.getIntValue("pushType"));
  290. assertEquals("FLASH_PICKUP_STOP_COMPLETED", body.getString("eventType"));
  291. assertEquals(41L, body.getLong("stopId"));
  292. assertEquals("PICKUP", body.getString("stopType"));
  293. assertEquals(2, body.getIntValue("stopOrder"));
  294. assertEquals(1, body.getIntValue("completedStopCount"));
  295. assertEquals(2, body.getIntValue("totalStopCount"));
  296. assertFalse(message.getBody().contains("secret"));
  297. assertEquals(1, fixture.sentContents.size());
  298. assertTrue(fixture.sentContents.get(0).contains("stop 3"));
  299. assertTrue(fixture.sentContents.get(0).contains("1/2"));
  300. }
  301. }
  302. @Test
  303. void deliveryStopProgressKeepsRealPartialAndFinalOrderStatesWithoutWholeOrderScene() {
  304. try (StopNotificationFixture fixture = new StopNotificationFixture()) {
  305. FlashDeliveryOrder partial = order();
  306. partial.setStatus("PICKED_UP");
  307. assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(partial, stop("DELIVERY", 3), 1, 2));
  308. FlashDeliveryOrder finished = order();
  309. finished.setStatus("DELIVERED");
  310. assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(finished, stop("DELIVERY", 4), 2, 2));
  311. fixture.runTasks();
  312. assertEquals(2, fixture.messages.size());
  313. List<JSONObject> bodies = fixture.messages.stream().map(message -> JSON.parseObject(message.getBody())).toList();
  314. assertEquals(List.of("PICKED_UP", "DELIVERED"), bodies.stream().map(body -> body.getString("state")).toList());
  315. assertEquals(List.of(1, 2), bodies.stream().map(body -> body.getIntValue("completedStopCount")).toList());
  316. for (JSONObject body : bodies) {
  317. assertEquals("FLASH_DELIVERY_STOP_COMPLETED", body.getString("eventType"));
  318. assertEquals("DELIVERY", body.getString("stopType"));
  319. assertEquals(2, body.getIntValue("totalStopCount"));
  320. assertFalse(body.containsKey("eventScene"));
  321. }
  322. assertEquals(List.of(7L, 7L), fixture.messages.stream().map(PushMessage::getUserId).toList());
  323. }
  324. }
  325. @Test
  326. void rollbackDoesNotQueueOrPersistEitherStopProgressNotification() {
  327. try (StopNotificationFixture fixture = new StopNotificationFixture()) {
  328. TransactionSynchronizationManager.initSynchronization();
  329. try {
  330. assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 0), 1, 2));
  331. assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2));
  332. TransactionSynchronizationManager.getSynchronizations()
  333. .forEach(sync -> sync.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
  334. fixture.runTasks();
  335. assertTrue(fixture.lookedUpUsers.isEmpty());
  336. assertTrue(fixture.tasks.isEmpty());
  337. assertTrue(fixture.messages.isEmpty());
  338. assertTrue(fixture.sentContents.isEmpty());
  339. } finally {
  340. TransactionSynchronizationManager.clearSynchronization();
  341. }
  342. }
  343. }
  344. @Test
  345. void missingDeviceAddressStillPersistsStopProgressThroughRealPayPushEventPath() {
  346. try (StopNotificationFixture fixture = new StopNotificationFixture()) {
  347. fixture.sender.setCid(null);
  348. for (BusinessPushProperties.Mode mode : BusinessPushProperties.Mode.values()) {
  349. fixture.properties.setMode(mode);
  350. assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 0), 1, 2));
  351. assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2));
  352. fixture.runTasks();
  353. }
  354. assertEquals(4, fixture.messages.size());
  355. assertTrue(fixture.messages.stream().allMatch(message -> Long.valueOf(7L).equals(message.getUserId())
  356. && Integer.valueOf(3).equals(message.getPushType())
  357. && "FL-20260813-021".equals(message.getDdId())));
  358. assertTrue(fixture.sentContents.isEmpty());
  359. }
  360. }
  361. @Test
  362. void stopProgressTextUsesSenderLocaleAndRouteNumberWithCompletedAndTotalCounts() {
  363. List<Locale> locales = List.of(Locale.ROOT, Locale.TAIWAN, Locale.SIMPLIFIED_CHINESE,
  364. Locale.US, Locale.forLanguageTag("th-TH"));
  365. List<String> pickupTexts = List.of("第 3 站已完成取貨,取貨進度 1/2", "第 3 站已完成取貨,取貨進度 1/2",
  366. "第 3 站已完成取货,取货进度 1/2", "Pickup completed at stop 3. Pickup progress: 1/2",
  367. "รับพัสดุที่จุด 3 แล้ว ความคืบหน้าการรับ: 1/2");
  368. List<String> deliveryTexts = List.of("第 5 站已送達,送達進度 2/2", "第 5 站已送達,送達進度 2/2",
  369. "第 5 站已送达,送达进度 2/2", "Delivery completed at stop 5. Delivery progress: 2/2",
  370. "ส่งพัสดุถึงจุด 5 แล้ว ความคืบหน้าการส่ง: 2/2");
  371. for (int i = 0; i < locales.size(); i++) {
  372. try (StopNotificationFixture fixture = new StopNotificationFixture()) {
  373. fixture.locale.when(() -> LocaleUtils.getUserLocale(7L)).thenReturn(locales.get(i));
  374. assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 2), 1, 2));
  375. assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 4), 2, 2));
  376. fixture.runTasks();
  377. assertEquals(2, fixture.messages.size());
  378. assertTrue(fixture.messages.get(0).getContent().contains(pickupTexts.get(i)));
  379. assertTrue(fixture.messages.get(1).getContent().contains(deliveryTexts.get(i)));
  380. assertTrue(fixture.messages.stream().allMatch(message -> !message.getContent().contains("{0}")
  381. && message.getContent().contains("FL-20260813-021")));
  382. }
  383. }
  384. }
  385. @Test
  386. void stopNotificationLookupAndSchedulingFailuresDoNotEscapeBusinessOperation() {
  387. try (StopNotificationFixture fixture = new StopNotificationFixture()) {
  388. when(fixture.users.selectById(7L)).thenThrow(new IllegalStateException("lookup failed"));
  389. assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 0), 1, 2));
  390. assertTrue(fixture.tasks.isEmpty());
  391. org.mockito.Mockito.doReturn(fixture.sender).when(fixture.users).selectById(7L);
  392. doThrow(new IllegalStateException("executor unavailable")).when(fixture.manager).execute(any(TimerTask.class));
  393. assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2));
  394. assertTrue(fixture.messages.isEmpty());
  395. }
  396. }
  397. @Test
  398. void stopNotificationPersistenceFailureIsContainedAndNextProgressStillSends() {
  399. try (StopNotificationFixture fixture = new StopNotificationFixture()) {
  400. fixture.persistenceFailure = true;
  401. assertDoesNotThrow(() -> fixture.service.notifyPickupStop(order(), stop("PICKUP", 0), 1, 2));
  402. assertDoesNotThrow(fixture::runTasks);
  403. assertTrue(fixture.messages.isEmpty());
  404. fixture.persistenceFailure = false;
  405. assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2));
  406. assertDoesNotThrow(fixture::runTasks);
  407. assertEquals(1, fixture.messages.size());
  408. assertEquals("FLASH_DELIVERY_STOP_COMPLETED", JSON.parseObject(fixture.messages.get(0).getBody()).getString("eventType"));
  409. }
  410. }
  411. @Test
  412. void externalStopPushFailureDoesNotLosePersistedProgressOrEscapeBusinessOperation() {
  413. try (StopNotificationFixture fixture = new StopNotificationFixture()) {
  414. doThrow(new IllegalStateException("network failure")).when(fixture.uni)
  415. .apppush(anyString(), anyString(), anyString(), anyString());
  416. assertDoesNotThrow(() -> fixture.service.notifyDeliveryStop(order(), stop("DELIVERY", 2), 1, 2));
  417. assertDoesNotThrow(fixture::runTasks);
  418. assertEquals(1, fixture.messages.size());
  419. assertEquals(7L, fixture.messages.get(0).getUserId());
  420. }
  421. }
  422. private static FlashDeliveryOrderStop stop(String type, int routeOrder) {
  423. FlashDeliveryOrderStop stop = new FlashDeliveryOrderStop();
  424. stop.setId(41L);
  425. stop.setOrderId(10L);
  426. stop.setStopType(type);
  427. stop.setStopOrder(routeOrder);
  428. stop.setReceiverUserId(99L);
  429. stop.setName("secret-contact");
  430. stop.setPhone("secret-phone");
  431. stop.setAddress("secret-address");
  432. stop.setDeliveryPinCode("secret-pin");
  433. return stop;
  434. }
  435. private static final class StopNotificationFixture implements AutoCloseable {
  436. final InfoUserMapper users = mock(InfoUserMapper.class);
  437. final InfoUser sender = new InfoUser();
  438. final PayPush uni = mock(PayPush.class);
  439. final BusinessPushProperties properties = new BusinessPushProperties();
  440. final List<Long> lookedUpUsers = new ArrayList<>();
  441. final List<TimerTask> tasks = new ArrayList<>();
  442. final List<PushMessage> messages = new ArrayList<>();
  443. final List<String> sentContents = new ArrayList<>();
  444. final MockedStatic<SpringUtils> spring;
  445. final MockedStatic<AsyncManager> async;
  446. final MockedStatic<LocaleUtils> locale;
  447. final AsyncManager manager;
  448. final FlashDeliveryNotificationService service;
  449. boolean persistenceFailure;
  450. StopNotificationFixture() {
  451. sender.setUserId(7L);
  452. sender.setCid("sender-cid");
  453. when(users.selectById(7L)).thenAnswer(call -> { lookedUpUsers.add(7L); return sender; });
  454. IInfoUserService currentUsers = mock(IInfoUserService.class);
  455. when(currentUsers.getById(7L)).thenReturn(sender);
  456. BusinessPushService router = new BusinessPushService(properties, currentUsers,
  457. mock(FcmPushSender.class), mock(ApnsPushSender.class), uni);
  458. IPushMessageService repository = mock(IPushMessageService.class);
  459. doAnswer(call -> {
  460. if (persistenceFailure) throw new IllegalStateException("message storage unavailable");
  461. messages.add(call.getArgument(0));
  462. return 1;
  463. }).when(repository).insertPushMessage(any(PushMessage.class));
  464. PushEventListener listener = new PushEventListener();
  465. ReflectionTestUtils.setField(listener, "pushMessageService", repository);
  466. PushEventService events = new PushEventService();
  467. ApplicationEventPublisher publisher = event -> listener.onApplicationEvent((PushEvent) event);
  468. ReflectionTestUtils.setField(events, "eventPublisher", publisher);
  469. ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
  470. messageSource.setBasename("i18n/messages");
  471. messageSource.setDefaultEncoding("UTF-8");
  472. messageSource.setFallbackToSystemLocale(false);
  473. spring = mockStatic(SpringUtils.class);
  474. spring.when(() -> SpringUtils.getBean("scheduledExecutorService")).thenReturn(mock(ScheduledExecutorService.class));
  475. spring.when(() -> SpringUtils.getBean(MessageSource.class)).thenReturn(messageSource);
  476. spring.when(() -> SpringUtils.getBean(BusinessPushService.class)).thenReturn(router);
  477. manager = mock(AsyncManager.class);
  478. doAnswer(call -> { tasks.add(call.getArgument(0)); return null; }).when(manager).execute(any(TimerTask.class));
  479. async = mockStatic(AsyncManager.class);
  480. async.when(AsyncManager::me).thenReturn(manager);
  481. locale = mockStatic(LocaleUtils.class);
  482. locale.when(() -> LocaleUtils.getUserLocale(7L)).thenReturn(Locale.US);
  483. doAnswer(call -> { sentContents.add(call.getArgument(2)); return null; }).when(uni)
  484. .apppush(eq("sender-cid"), anyString(), anyString(), anyString());
  485. service = new FlashDeliveryNotificationService(users, events);
  486. }
  487. void runTasks() {
  488. List<TimerTask> pending = new ArrayList<>(tasks);
  489. tasks.clear();
  490. pending.forEach(TimerTask::run);
  491. }
  492. @Override
  493. public void close() {
  494. locale.close();
  495. async.close();
  496. spring.close();
  497. }
  498. }
  499. private static FlashDeliveryOrder order() {
  500. FlashDeliveryOrder order = new FlashDeliveryOrder();
  501. order.setId(10L);
  502. order.setOrderNo("FL-20260813-021");
  503. order.setUserId(7L);
  504. return order;
  505. }
  506. }