LinePayCancellationRaceTest.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. package com.ruoyi.app.pay;
  2. import com.baomidou.mybatisplus.core.conditions.Wrapper;
  3. import com.ruoyi.system.domain.PosOrder;
  4. import com.ruoyi.system.domain.PosOrderLinePayment;
  5. import com.ruoyi.system.domain.PosOrderLineRefund;
  6. import com.ruoyi.system.service.IPosOrderLinePaymentService;
  7. import com.ruoyi.system.service.IPosOrderLineRefundService;
  8. import com.ruoyi.system.service.IPosOrderService;
  9. import com.ruoyi.system.mapper.PosOrderMapper;
  10. import com.ruoyi.app.order.OrderLifecycleService;
  11. import org.junit.jupiter.api.Test;
  12. import java.util.Date;
  13. import static org.junit.jupiter.api.Assertions.assertEquals;
  14. import static org.mockito.ArgumentMatchers.any;
  15. import static org.mockito.ArgumentMatchers.eq;
  16. import static org.mockito.Mockito.mock;
  17. import static org.mockito.Mockito.never;
  18. import static org.mockito.Mockito.verify;
  19. import static org.mockito.Mockito.when;
  20. import static org.mockito.Mockito.times;
  21. class LinePayCancellationRaceTest {
  22. @Test
  23. void offlineCancellationFirstThenLateCaptureCreatesOneDurableRefundIntent() {
  24. IPosOrderService orders = mock(IPosOrderService.class);
  25. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  26. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  27. PosOrderLinePayment payment = payment("CONFIRM_UNKNOWN");
  28. payment.setPaymentMode("OFFLINE");
  29. payment.setLineOrderId("LPOFF-1");
  30. when(payments.getById(70L)).thenReturn(payment);
  31. when(payments.markPaid(eq(70L), eq(2L), eq("CONFIRM_UNKNOWN"), eq("TX-1"),
  32. any(), any())).thenReturn(1);
  33. PosOrder cancelled = order(4L);
  34. when(orders.getOne(any(Wrapper.class))).thenReturn(cancelled);
  35. LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
  36. facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
  37. verify(refunds).createIfAbsent(payment, "TASK");
  38. assertEquals("PAID", payment.getStatus());
  39. }
  40. @Test
  41. void tableOrderStateOneAcceptsCaptureWithoutCompensationRefund() {
  42. IPosOrderService orders = mock(IPosOrderService.class);
  43. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  44. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  45. PosOrderLinePayment payment = payment("WAITING_AUTH");
  46. when(payments.getById(70L)).thenReturn(payment);
  47. when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
  48. any(), any())).thenReturn(1);
  49. when(orders.getOne(any(Wrapper.class))).thenReturn(order(1L));
  50. LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
  51. facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
  52. verify(refunds, never()).createIfAbsent(any(), any());
  53. }
  54. @Test
  55. void paidFirstThenCancellationUsesUniqueRefundRow() {
  56. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  57. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  58. PosOrderLinePayment payment = payment("PAID");
  59. when(payments.getById(70L)).thenReturn(payment);
  60. PosOrderLineRefund existing = new PosOrderLineRefund();
  61. existing.setPaymentId(70L);
  62. existing.setStatus("CREATED");
  63. when(refunds.createIfAbsent(payment, "USER_CANCEL")).thenReturn(existing);
  64. LinePayRefundService service = new LinePayRefundService(payments, refunds,
  65. mock(com.ruoyi.system.service.IPosStoreLinePayService.class),
  66. mock(com.ruoyi.app.utils.linepay.LinePayClient.class), mock(LinePayFactService.class));
  67. assertEquals("CREATED", service.createRefundIntent(70L, "USER_CANCEL"));
  68. assertEquals("CREATED", service.createRefundIntent(70L, "USER_CANCEL"));
  69. verify(refunds, org.mockito.Mockito.times(2)).createIfAbsent(payment, "USER_CANCEL");
  70. }
  71. @Test
  72. void lateDuplicateCaptureCreatesCompensationRefundWhenOrderWasAlreadyPaidElsewhere() {
  73. IPosOrderService orders = mock(IPosOrderService.class);
  74. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  75. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  76. PosOrderMapper orderMapper = mock(PosOrderMapper.class);
  77. PosOrderLinePayment payment = payment("FAILED");
  78. when(payments.getById(70L)).thenReturn(payment);
  79. when(payments.markPaid(eq(70L), eq(2L), eq("FAILED"), eq("TX-1"),
  80. any(), any())).thenReturn(1);
  81. PosOrder order = order(1L);
  82. order.setPayType("2");
  83. order.setPayStatus(1L);
  84. when(orders.getOne(any(Wrapper.class))).thenReturn(order);
  85. when(orderMapper.markLinePaid("DD-1", 70L)).thenReturn(0);
  86. LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper);
  87. facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
  88. verify(refunds).createIfAbsent(payment, "DUPLICATE_PAYMENT");
  89. }
  90. @Test
  91. void firstSuccessfulOrderCaptureTriggersNotificationOnlyOnce() {
  92. IPosOrderService orders = mock(IPosOrderService.class);
  93. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  94. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  95. PosOrderMapper orderMapper = mock(PosOrderMapper.class);
  96. LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
  97. PosOrderLinePayment payment = payment("WAITING_AUTH");
  98. when(payments.getById(70L)).thenReturn(payment);
  99. when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
  100. any(), any())).thenReturn(1);
  101. PosOrder order = order(1L);
  102. when(orders.getOne(any(Wrapper.class))).thenReturn(order);
  103. when(orderMapper.markLinePaid("DD-1", 70L)).thenReturn(1, 0);
  104. LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
  105. notifications, null);
  106. facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
  107. payment.setStatus("PAID");
  108. facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
  109. verify(notifications, times(1)).paymentCaptured(order);
  110. }
  111. @Test
  112. void alreadyRefundedRetrieveDoesNotNotifyOrderAsNewlyPaid() {
  113. IPosOrderService orders = mock(IPosOrderService.class);
  114. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  115. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  116. PosOrderMapper orderMapper = mock(PosOrderMapper.class);
  117. LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
  118. PosOrderLinePayment payment = payment("WAITING_AUTH");
  119. when(payments.getById(70L)).thenReturn(payment);
  120. when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
  121. any(), any())).thenReturn(1);
  122. PosOrder order = order(1L);
  123. when(orders.getOne(any(Wrapper.class))).thenReturn(order);
  124. when(orderMapper.markLinePaid("DD-1", 70L)).thenReturn(1);
  125. when(orderMapper.markLineRefunded("DD-1", 70L)).thenReturn(1);
  126. PosOrderLineRefund refund = new PosOrderLineRefund();
  127. refund.setId(80L);
  128. refund.setPaymentId(70L);
  129. refund.setStatus("CREATED");
  130. refund.setVersion(1L);
  131. when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(refund);
  132. when(refunds.markRefundedFromEvidence(eq(80L), eq(1L), eq("CREATED"),
  133. eq("RF-1"), any())).thenReturn(1);
  134. LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
  135. notifications, null);
  136. facts.applyPaidAndRefundedFact(70L, "TX-1", "TSP", new Date(), "RF-1", new Date());
  137. verify(notifications, never()).paymentCaptured(any());
  138. }
  139. @Test
  140. void verifiedFullRefundEvidenceEscalatesPriorFailedRefund() {
  141. IPosOrderService orders = mock(IPosOrderService.class);
  142. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  143. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  144. PosOrderMapper orderMapper = mock(PosOrderMapper.class);
  145. PosOrderLinePayment payment = payment("PAID");
  146. when(payments.getById(70L)).thenReturn(payment);
  147. when(payments.markPaid(eq(70L), eq(2L), eq("PAID"), eq("TX-1"),
  148. any(), any())).thenReturn(1);
  149. PosOrder order = order(1L);
  150. order.setPayStatus(1L);
  151. when(orders.getOne(any(Wrapper.class))).thenReturn(order);
  152. when(orderMapper.markLineRefunded("DD-1", 70L)).thenReturn(1);
  153. PosOrderLineRefund failed = new PosOrderLineRefund();
  154. failed.setId(80L);
  155. failed.setPaymentId(70L);
  156. failed.setStatus("FAILED");
  157. failed.setVersion(3L);
  158. when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(failed);
  159. when(refunds.markRefundedFromEvidence(eq(80L), eq(3L), eq("FAILED"),
  160. eq("RF-EXTERNAL"), any())).thenReturn(1);
  161. LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
  162. mock(LinePayOrderNotificationService.class), null);
  163. facts.applyPaidAndRefundedFact(70L, "TX-1", "TSP", new Date(),
  164. "RF-EXTERNAL", new Date());
  165. verify(refunds).markRefundedFromEvidence(eq(80L), eq(3L), eq("FAILED"),
  166. eq("RF-EXTERNAL"), any());
  167. verify(refunds, never()).markRefunded(any(), any(), any(), any());
  168. }
  169. @Test
  170. void partialRefundEvidenceRecordsPaymentFactWithoutFulfillingOrder() {
  171. IPosOrderService orders = mock(IPosOrderService.class);
  172. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  173. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  174. PosOrderMapper orderMapper = mock(PosOrderMapper.class);
  175. LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
  176. PosOrderLinePayment payment = payment("WAITING_AUTH");
  177. when(payments.getById(70L)).thenReturn(payment);
  178. when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
  179. any(), any())).thenReturn(1);
  180. PosOrderLineRefund refund = new PosOrderLineRefund();
  181. refund.setId(80L);
  182. refund.setPaymentId(70L);
  183. refund.setStatus("CREATED");
  184. refund.setVersion(1L);
  185. when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(refund);
  186. when(refunds.markManualReviewFromEvidence(80L, 1L, "CREATED")).thenReturn(1);
  187. LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
  188. notifications, null);
  189. facts.applyPaidWithRefundReviewFact(70L, "TX-1", "TSP", new Date());
  190. verify(orderMapper, never()).markLinePaid(any(), any());
  191. verify(notifications, never()).paymentCaptured(any());
  192. verify(refunds).markManualReviewFromEvidence(80L, 1L, "CREATED");
  193. }
  194. @Test
  195. void ambiguousLaterEvidenceNeverDowngradesConfirmedRefund() {
  196. IPosOrderService orders = mock(IPosOrderService.class);
  197. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  198. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  199. PosOrderLinePayment payment = payment("PAID");
  200. when(payments.getById(70L)).thenReturn(payment);
  201. when(payments.markPaid(eq(70L), eq(2L), eq("PAID"), eq("TX-1"),
  202. any(), any())).thenReturn(1);
  203. PosOrderLineRefund refunded = new PosOrderLineRefund();
  204. refunded.setId(80L);
  205. refunded.setPaymentId(70L);
  206. refunded.setStatus("REFUNDED");
  207. refunded.setVersion(3L);
  208. when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(refunded);
  209. LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
  210. facts.applyPaidWithRefundReviewFact(70L, "TX-1", "TSP", new Date());
  211. verify(refunds, never()).markManualReview(any(), any(), any());
  212. }
  213. @Test
  214. void verifiedPartialRefundEvidenceEscalatesPriorFailedRefund() {
  215. IPosOrderService orders = mock(IPosOrderService.class);
  216. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  217. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  218. PosOrderLinePayment payment = payment("PAID");
  219. when(payments.getById(70L)).thenReturn(payment);
  220. when(payments.markPaid(eq(70L), eq(2L), eq("PAID"), eq("TX-1"),
  221. any(), any())).thenReturn(1);
  222. PosOrderLineRefund failed = new PosOrderLineRefund();
  223. failed.setId(80L);
  224. failed.setPaymentId(70L);
  225. failed.setStatus("FAILED");
  226. failed.setVersion(3L);
  227. when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(failed);
  228. when(refunds.markManualReviewFromEvidence(80L, 3L, "FAILED")).thenReturn(1);
  229. LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
  230. facts.applyPaidWithRefundReviewFact(70L, "TX-1", "TSP", new Date());
  231. verify(refunds).markManualReviewFromEvidence(80L, 3L, "FAILED");
  232. verify(refunds, never()).markManualReview(any(), any(), any());
  233. }
  234. @Test
  235. void cancelledLateCaptureCanCommitFullRefundWithoutPaymentNotification() {
  236. IPosOrderService orders = mock(IPosOrderService.class);
  237. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  238. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  239. PosOrderMapper orderMapper = mock(PosOrderMapper.class);
  240. LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
  241. OrderLifecycleService lifecycle = mock(OrderLifecycleService.class);
  242. PosOrderLinePayment payment = payment("CONFIRM_UNKNOWN");
  243. when(payments.getById(70L)).thenReturn(payment);
  244. when(payments.markPaid(eq(70L), eq(2L), eq("CONFIRM_UNKNOWN"), eq("TX-1"),
  245. any(), any())).thenReturn(1);
  246. PosOrder cancelled = order(4L);
  247. when(orders.getOne(any(Wrapper.class))).thenReturn(cancelled);
  248. when(orderMapper.markLinePaid("DD-1", 70L)).thenAnswer(invocation -> {
  249. cancelled.setPayStatus(1L);
  250. return 1;
  251. });
  252. PosOrderLineRefund refund = new PosOrderLineRefund();
  253. refund.setId(80L);
  254. refund.setPaymentId(70L);
  255. refund.setStatus("CREATED");
  256. refund.setVersion(1L);
  257. when(refunds.createIfAbsent(payment, "TASK")).thenReturn(refund);
  258. when(refunds.markRefunded(eq(80L), eq(1L), eq("RF-1"), any())).thenReturn(1);
  259. when(orderMapper.markLineRefunded("DD-1", 70L)).thenAnswer(invocation -> {
  260. cancelled.setPayStatus(2L);
  261. return 1;
  262. });
  263. LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
  264. notifications, lifecycle);
  265. facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
  266. facts.applyRefundedFact(80L, 70L, 1L, "RF-1", new Date());
  267. assertEquals(2L, cancelled.getPayStatus());
  268. verify(notifications, never()).paymentCaptured(any());
  269. verify(lifecycle).finalizeSystemLineRefund(9L);
  270. }
  271. @Test
  272. void amountMismatchFactCreatesRefundIntentOnlyAfterEvidenceCas() {
  273. IPosOrderService orders = mock(IPosOrderService.class);
  274. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  275. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  276. PosOrderLinePayment persisted = payment("AMOUNT_MISMATCH");
  277. persisted.setCapturedAmount(100);
  278. when(payments.markOfflineEvidence(eq(70L), eq(2L), eq("REQUESTING"),
  279. eq("AMOUNT_MISMATCH"), eq("TX-1"), eq("TSP"), eq(100), any(Date.class)))
  280. .thenReturn(1);
  281. when(payments.getById(70L)).thenReturn(persisted);
  282. LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
  283. assertEquals(true, facts.applyAmountMismatchFact(70L, 2L, "REQUESTING",
  284. "TX-1", "TSP", 100, new Date()));
  285. verify(refunds).createIfAbsent(persisted, "AMOUNT_MISMATCH");
  286. }
  287. @Test
  288. void failedAmountMismatchEvidenceCasNeverCreatesRefundIntent() {
  289. IPosOrderService orders = mock(IPosOrderService.class);
  290. IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
  291. IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
  292. when(payments.markOfflineEvidence(eq(70L), eq(2L), eq("REQUESTING"),
  293. eq("AMOUNT_MISMATCH"), eq("TX-1"), eq("TSP"), eq(100), any(Date.class)))
  294. .thenReturn(0);
  295. LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
  296. assertEquals(false, facts.applyAmountMismatchFact(70L, 2L, "REQUESTING",
  297. "TX-1", "TSP", 100, new Date()));
  298. verify(refunds, never()).createIfAbsent(any(), any());
  299. }
  300. private static PosOrderLinePayment payment(String status) {
  301. PosOrderLinePayment payment = new PosOrderLinePayment();
  302. payment.setId(70L);
  303. payment.setDdId("DD-1");
  304. payment.setTransactionId("TX-1");
  305. payment.setStatus(status);
  306. payment.setVersion(2L);
  307. return payment;
  308. }
  309. private static PosOrder order(Long state) {
  310. PosOrder order = new PosOrder();
  311. order.setId(9L);
  312. order.setDdId("DD-1");
  313. order.setState(state);
  314. order.setPayType("3");
  315. order.setPayStatus(0L);
  316. order.setAfterSaleStatus(0L);
  317. return order;
  318. }
  319. }