OmgPaymentNotifyServiceTest.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. package com.ruoyi.app.omgpay;
  2. import com.ruoyi.app.order.DeliveryOrderNotificationService;
  3. import com.ruoyi.app.push.BusinessPushService;
  4. import com.ruoyi.app.push.RecipientApp;
  5. import com.ruoyi.app.omgpay.dto.OmgNotifyField;
  6. import com.ruoyi.app.omgpay.dto.OmgNotifyRequest;
  7. import com.ruoyi.app.utils.event.PushEventService;
  8. import com.ruoyi.common.utils.spring.SpringUtils;
  9. import com.ruoyi.system.domain.InfoUser;
  10. import com.ruoyi.system.domain.PosOrder;
  11. import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt;
  12. import com.ruoyi.system.omgpay.domain.OmgPaymentOrderSnapshot;
  13. import com.ruoyi.system.omgpay.service.IOmgPaymentAttemptService;
  14. import com.ruoyi.system.service.IInfoUserService;
  15. import com.ruoyi.system.service.IPosOrderService;
  16. import org.junit.jupiter.api.AfterAll;
  17. import org.junit.jupiter.api.BeforeAll;
  18. import org.junit.jupiter.api.BeforeEach;
  19. import org.junit.jupiter.api.Test;
  20. import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
  21. import org.springframework.beans.factory.support.DefaultListableBeanFactory;
  22. import org.springframework.context.support.StaticMessageSource;
  23. import org.springframework.test.util.ReflectionTestUtils;
  24. import java.util.ArrayList;
  25. import java.math.BigDecimal;
  26. import java.util.LinkedHashMap;
  27. import java.util.List;
  28. import java.util.Locale;
  29. import java.util.Map;
  30. import static org.junit.jupiter.api.Assertions.*;
  31. import static org.mockito.ArgumentMatchers.any;
  32. import static org.mockito.ArgumentMatchers.anyString;
  33. import static org.mockito.ArgumentMatchers.eq;
  34. import static org.mockito.Mockito.*;
  35. class OmgPaymentNotifyServiceTest {
  36. private static final String HASH_KEY = "5294y06JbISpM5x9";
  37. private static final String HASH_IV = "v77hoKGq4kWxNNIS";
  38. private static ConfigurableListableBeanFactory originalBeanFactory;
  39. private IOmgPaymentAttemptService attempts;
  40. private OmgPaymentNotifyService service;
  41. @BeforeAll
  42. static void initializeBeanFactory() {
  43. // 支付成功推送按收件人用户语言解析文案(zh_TW 为无上下文时的默认语言),纯单测下需手动挂 messageSource
  44. originalBeanFactory = (ConfigurableListableBeanFactory)
  45. ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
  46. DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
  47. StaticMessageSource messageSource = new StaticMessageSource();
  48. Locale defaultLocale = new Locale("zh", "TW");
  49. messageSource.addMessage("no.message.push.message", defaultLocale, "消息");
  50. messageSource.addMessage("no.message.push.payment.success", defaultLocale, "支付成功");
  51. beanFactory.registerSingleton("messageSource", messageSource);
  52. new SpringUtils().postProcessBeanFactory(beanFactory);
  53. }
  54. @AfterAll
  55. static void restoreBeanFactory() {
  56. new SpringUtils().postProcessBeanFactory(originalBeanFactory);
  57. }
  58. private OmgPaymentAttempt attempt;
  59. @BeforeEach
  60. void setUp() {
  61. attempts = mock(IOmgPaymentAttemptService.class);
  62. service = new OmgPaymentNotifyService(attempts, new OmgCheckMacSigner());
  63. attempt = new OmgPaymentAttempt();
  64. attempt.setId(7L);
  65. attempt.setDdId("DD-1");
  66. attempt.setMerchantTradeNo("OMG123");
  67. attempt.setMerchantId("1000031");
  68. attempt.setAmount(100);
  69. attempt.setHashKeySnapshot(HASH_KEY);
  70. attempt.setHashIvSnapshot(HASH_IV);
  71. attempt.setAttemptStatus(0);
  72. when(attempts.selectByMerchantTradeNo("OMG123")).thenReturn(attempt);
  73. when(attempts.selectByMerchantTradeNoForUpdate("OMG123")).thenReturn(attempt);
  74. }
  75. @Test
  76. void successfulCallbackUsesSnapshotAndMarksOnlyPaymentFacts() {
  77. OmgPaymentOrderSnapshot order = order(4L, 0L);
  78. when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);
  79. when(attempts.markPaid(any())).thenReturn(1);
  80. when(attempts.markOrderPaid("DD-1")).thenReturn(1);
  81. assertTrue(service.process(signedRequest(1, 0, 100)));
  82. var facts = org.mockito.ArgumentCaptor.forClass(OmgPaymentAttempt.class);
  83. verify(attempts).markPaid(facts.capture());
  84. assertEquals("GW123", facts.getValue().getTradeNo());
  85. assertEquals(1, facts.getValue().getRtnCode());
  86. assertEquals(0, facts.getValue().getSimulatePaid());
  87. verify(attempts).supersedeOtherCreated("DD-1", 7L);
  88. verify(attempts).markOrderPaid("DD-1");
  89. }
  90. @Test
  91. void simulatedSuccessOnCancelledOrderIsStillPaid() {
  92. when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(4L, 0L));
  93. when(attempts.markPaid(any())).thenReturn(1);
  94. when(attempts.markOrderPaid("DD-1")).thenReturn(1);
  95. when(attempts.countOtherPaidAttempts("DD-1", 7L)).thenReturn(1);
  96. assertTrue(service.process(signedRequest(1, 1, 100)));
  97. var facts = org.mockito.ArgumentCaptor.forClass(OmgPaymentAttempt.class);
  98. verify(attempts).markPaid(facts.capture());
  99. assertEquals(1, facts.getValue().getSimulatePaid());
  100. }
  101. @Test
  102. void failureMarksAttemptFailedWithoutChangingOrder() {
  103. when(attempts.markFailed(any())).thenReturn(1);
  104. assertTrue(service.process(signedRequest(10200095, 0, 100)));
  105. verify(attempts).markFailed(any());
  106. verify(attempts, never()).markOrderPaid(anyString());
  107. }
  108. @Test
  109. void supersededAttemptAcceptsFailureWithoutLosingSupersededState() {
  110. attempt.setAttemptStatus(3);
  111. assertTrue(service.process(signedRequest(10200095, 0, 100)));
  112. verify(attempts, never()).markFailed(any());
  113. verify(attempts, never()).markOrderPaid(anyString());
  114. }
  115. @Test
  116. void paidAttemptCannotBeDowngradedByLateFailure() {
  117. attempt.setAttemptStatus(1);
  118. assertTrue(service.process(signedRequest(10200095, 0, 100)));
  119. verify(attempts, never()).markFailed(any());
  120. verify(attempts, never()).markOrderPaid(anyString());
  121. }
  122. @Test
  123. void failedAttemptMayLaterBecomePaid() {
  124. attempt.setAttemptStatus(2);
  125. when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 0L));
  126. when(attempts.markPaid(any())).thenReturn(1);
  127. when(attempts.markOrderPaid("DD-1")).thenReturn(1);
  128. assertTrue(service.process(signedRequest(1, 0, 100)));
  129. verify(attempts).markPaid(any());
  130. verify(attempts).markOrderPaid("DD-1");
  131. }
  132. @Test
  133. void paidDeliveryOrderIsOpenedToRidersAfterOmgSettlement() {
  134. IPosOrderService orders = mock(IPosOrderService.class);
  135. DeliveryOrderNotificationService notifications = mock(DeliveryOrderNotificationService.class);
  136. ReflectionTestUtils.setField(service, "posOrderService", orders);
  137. ReflectionTestUtils.setField(service, "deliveryOrderNotificationService", notifications);
  138. when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 0L));
  139. when(attempts.markPaid(any())).thenReturn(1);
  140. when(attempts.markOrderPaid("DD-1")).thenReturn(1);
  141. PosOrder paidOrder = new PosOrder();
  142. paidOrder.setDdId("DD-1");
  143. paidOrder.setType(0L);
  144. paidOrder.setState(0L);
  145. paidOrder.setDeliveryStatus(0L);
  146. paidOrder.setAfterSaleStatus(0L);
  147. when(orders.getOne(any())).thenReturn(paidOrder);
  148. assertTrue(service.process(signedRequest(1, 0, 100)));
  149. assertEquals(1L, paidOrder.getPayStatus());
  150. verify(notifications).notifyOrderAvailable(paidOrder);
  151. }
  152. @Test
  153. void firstPaidCallbackPushesPaymentSuccessToOrderUser() {
  154. IInfoUserService users = mock(IInfoUserService.class);
  155. PushEventService pushEvents = mock(PushEventService.class);
  156. BusinessPushService push = mock(BusinessPushService.class);
  157. ReflectionTestUtils.setField(service, "infoUserService", users);
  158. ReflectionTestUtils.setField(service, "pushEventService", pushEvents);
  159. ReflectionTestUtils.setField(service, "businessPushService", push);
  160. OmgPaymentOrderSnapshot order = order(0L, 0L);
  161. order.setUserId(9L);
  162. when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);
  163. when(attempts.markPaid(any())).thenReturn(1);
  164. when(attempts.markOrderPaid("DD-1")).thenReturn(1);
  165. InfoUser user = new InfoUser();
  166. user.setUserId(9L); // cid 为空时仍需走统一路由,由当前模式决定能否外发
  167. user.setCidType("ios");
  168. user.setDeviceToken("native-token");
  169. when(users.getById(9L)).thenReturn(user);
  170. assertTrue(service.process(signedRequest(1, 0, 100)));
  171. verify(pushEvents).PublisherEvent(eq(9L), anyString(), anyString(), anyString());
  172. verify(push).send(eq(RecipientApp.USER), eq(9L), eq(null), anyString(), anyString(), anyString());
  173. }
  174. @Test
  175. void duplicatePaidCallbackDoesNotPushPaymentSuccessAgain() {
  176. IInfoUserService users = mock(IInfoUserService.class);
  177. PushEventService pushEvents = mock(PushEventService.class);
  178. ReflectionTestUtils.setField(service, "infoUserService", users);
  179. ReflectionTestUtils.setField(service, "pushEventService", pushEvents);
  180. attempt.setAttemptStatus(1); // 已支付 attempt:重复回调走 duplicate 分支
  181. when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 1L));
  182. InfoUser user = new InfoUser();
  183. user.setUserId(9L);
  184. when(users.getById(9L)).thenReturn(user);
  185. assertTrue(service.process(signedRequest(1, 0, 100)));
  186. verify(pushEvents, never()).PublisherEvent(any(), anyString(), anyString(), anyString());
  187. }
  188. @Test
  189. void rejectsSignatureMerchantAndAmountMismatchWithoutMutation() {
  190. OmgNotifyRequest badSignature = signedRequest(1, 0, 100);
  191. List<OmgNotifyField> changed = new ArrayList<>(badSignature.getFields());
  192. changed.set(changed.size() - 1, new OmgNotifyField("CheckMacValue", "0".repeat(64)));
  193. assertFalse(service.process(new OmgNotifyRequest("127.0.0.1", "raw", changed, null)));
  194. assertFalse(service.process(signedRequest(1, 0, 101)));
  195. assertFalse(service.process(signedRequest(1, 0, 100, "OTHER")));
  196. verify(attempts, never()).markPaid(any());
  197. verify(attempts, never()).markFailed(any());
  198. }
  199. @Test
  200. void validExtraFieldIsRequiredBySignatureVerification() {
  201. OmgNotifyRequest signed = signedRequest(1, 0, 100);
  202. List<OmgNotifyField> fieldsWithoutExtra = signed.getFields().stream()
  203. .filter(field -> !"FutureEmptyField".equals(field.name())).toList();
  204. assertFalse(service.process(new OmgNotifyRequest("127.0.0.1", "raw", fieldsWithoutExtra, null)));
  205. verify(attempts, never()).markPaid(any());
  206. }
  207. @Test
  208. void verifiedPaidQueryUsesTheSameIrreversibleSettlementAsCallback() {
  209. when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 0L));
  210. when(attempts.selectByMerchantTradeNoForUpdate("OMG123")).thenReturn(attempt);
  211. when(attempts.markPaid(any())).thenReturn(1);
  212. when(attempts.markOrderPaid("DD-1")).thenReturn(1);
  213. OmgPaymentGatewayFacts facts = new OmgPaymentGatewayFacts(
  214. OmgPaymentFactSource.QUERY, "1000031", "OMG123", 100, 1,
  215. "TradeStatus=1", "GW123", "Credit_CreditCard", null, null,
  216. new BigDecimal("0.00"), null);
  217. assertEquals(OmgPaymentSettlementResult.PAID, service.synchronizeVerifiedQuery(facts));
  218. verify(attempts).markPaid(any());
  219. verify(attempts).supersedeOtherCreated("DD-1", 7L);
  220. verify(attempts).markOrderPaid("DD-1");
  221. }
  222. @Test
  223. void failedQueryCannotDowngradeAnAlreadyPaidAttempt() {
  224. attempt.setAttemptStatus(1);
  225. when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 1L));
  226. when(attempts.selectByMerchantTradeNoForUpdate("OMG123")).thenReturn(attempt);
  227. OmgPaymentGatewayFacts facts = new OmgPaymentGatewayFacts(
  228. OmgPaymentFactSource.QUERY, "1000031", "OMG123", 100, 10200095,
  229. "TradeStatus=10200095", null, null, null, null,
  230. new BigDecimal("0.00"), null);
  231. assertEquals(OmgPaymentSettlementResult.PAID, service.synchronizeVerifiedQuery(facts));
  232. verify(attempts, never()).markFailed(any());
  233. }
  234. private OmgNotifyRequest signedRequest(int rtnCode, int simulatePaid, int amount) {
  235. return signedRequest(rtnCode, simulatePaid, amount, "1000031");
  236. }
  237. private OmgNotifyRequest signedRequest(int rtnCode, int simulatePaid, int amount, String merchantId) {
  238. Map<String, String> fields = new LinkedHashMap<>();
  239. fields.put("MerchantID", merchantId);
  240. fields.put("MerchantTradeNo", "OMG123");
  241. fields.put("StoreID", "");
  242. fields.put("RtnCode", String.valueOf(rtnCode));
  243. fields.put("RtnMsg", rtnCode == 1 ? "Succeeded" : "Failed");
  244. fields.put("TradeNo", "GW123");
  245. fields.put("TradeAmt", String.valueOf(amount));
  246. fields.put("PaymentDate", rtnCode == 1 ? "2026/08/13 12:00:00" : "");
  247. fields.put("PaymentType", "Credit_CreditCard");
  248. fields.put("PaymentTypeChargeFee", "0");
  249. fields.put("TradeDate", "2026/08/13 11:59:00");
  250. fields.put("SimulatePaid", String.valueOf(simulatePaid));
  251. fields.put("CustomField1", "");
  252. fields.put("CustomField2", "");
  253. fields.put("CustomField3", "");
  254. fields.put("CustomField4", "");
  255. fields.put("FutureEmptyField", "");
  256. fields.put("CheckMacValue", new OmgCheckMacSigner().sign(fields, HASH_KEY, HASH_IV));
  257. List<OmgNotifyField> decoded = fields.entrySet().stream()
  258. .map(entry -> new OmgNotifyField(entry.getKey(), entry.getValue())).toList();
  259. return new OmgNotifyRequest("127.0.0.1", "raw", decoded, null);
  260. }
  261. private static OmgPaymentOrderSnapshot order(long state, long payStatus) {
  262. OmgPaymentOrderSnapshot order = new OmgPaymentOrderSnapshot();
  263. order.setDdId("DD-1");
  264. order.setState(state);
  265. order.setPayStatus(payStatus);
  266. return order;
  267. }
  268. }