| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- package com.ruoyi.app.pay;
- import com.baomidou.mybatisplus.core.conditions.Wrapper;
- import com.ruoyi.system.domain.PosOrder;
- import com.ruoyi.system.domain.PosOrderLinePayment;
- import com.ruoyi.system.domain.PosOrderLineRefund;
- import com.ruoyi.system.service.IPosOrderLinePaymentService;
- import com.ruoyi.system.service.IPosOrderLineRefundService;
- import com.ruoyi.system.service.IPosOrderService;
- import com.ruoyi.system.mapper.PosOrderMapper;
- import com.ruoyi.app.order.OrderLifecycleService;
- import org.junit.jupiter.api.Test;
- import java.util.Date;
- import static org.junit.jupiter.api.Assertions.assertEquals;
- import static org.mockito.ArgumentMatchers.any;
- import static org.mockito.ArgumentMatchers.eq;
- import static org.mockito.Mockito.mock;
- import static org.mockito.Mockito.never;
- import static org.mockito.Mockito.verify;
- import static org.mockito.Mockito.when;
- import static org.mockito.Mockito.times;
- class LinePayCancellationRaceTest {
- @Test
- void offlineCancellationFirstThenLateCaptureCreatesOneDurableRefundIntent() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderLinePayment payment = payment("CONFIRM_UNKNOWN");
- payment.setPaymentMode("OFFLINE");
- payment.setLineOrderId("LPOFF-1");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("CONFIRM_UNKNOWN"), eq("TX-1"),
- any(), any())).thenReturn(1);
- PosOrder cancelled = order(4L);
- when(orders.getOne(any(Wrapper.class))).thenReturn(cancelled);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
- facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
- verify(refunds).createIfAbsent(payment, "TASK");
- assertEquals("PAID", payment.getStatus());
- }
- @Test
- void tableOrderStateOneAcceptsCaptureWithoutCompensationRefund() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderLinePayment payment = payment("WAITING_AUTH");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
- any(), any())).thenReturn(1);
- when(orders.getOne(any(Wrapper.class))).thenReturn(order(1L));
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
- facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
- verify(refunds, never()).createIfAbsent(any(), any());
- }
- @Test
- void paidFirstThenCancellationUsesUniqueRefundRow() {
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderLinePayment payment = payment("PAID");
- when(payments.getById(70L)).thenReturn(payment);
- PosOrderLineRefund existing = new PosOrderLineRefund();
- existing.setPaymentId(70L);
- existing.setStatus("CREATED");
- when(refunds.createIfAbsent(payment, "USER_CANCEL")).thenReturn(existing);
- LinePayRefundService service = new LinePayRefundService(payments, refunds,
- mock(com.ruoyi.system.service.IPosStoreLinePayService.class),
- mock(com.ruoyi.app.utils.linepay.LinePayClient.class), mock(LinePayFactService.class));
- assertEquals("CREATED", service.createRefundIntent(70L, "USER_CANCEL"));
- assertEquals("CREATED", service.createRefundIntent(70L, "USER_CANCEL"));
- verify(refunds, org.mockito.Mockito.times(2)).createIfAbsent(payment, "USER_CANCEL");
- }
- @Test
- void lateDuplicateCaptureCreatesCompensationRefundWhenOrderWasAlreadyPaidElsewhere() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderMapper orderMapper = mock(PosOrderMapper.class);
- PosOrderLinePayment payment = payment("FAILED");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("FAILED"), eq("TX-1"),
- any(), any())).thenReturn(1);
- PosOrder order = order(1L);
- order.setPayType("2");
- order.setPayStatus(1L);
- when(orders.getOne(any(Wrapper.class))).thenReturn(order);
- when(orderMapper.markLinePaid("DD-1", 70L)).thenReturn(0);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper);
- facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
- verify(refunds).createIfAbsent(payment, "DUPLICATE_PAYMENT");
- }
- @Test
- void firstSuccessfulOrderCaptureTriggersNotificationOnlyOnce() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderMapper orderMapper = mock(PosOrderMapper.class);
- LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
- PosOrderLinePayment payment = payment("WAITING_AUTH");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
- any(), any())).thenReturn(1);
- PosOrder order = order(1L);
- when(orders.getOne(any(Wrapper.class))).thenReturn(order);
- when(orderMapper.markLinePaid("DD-1", 70L)).thenReturn(1, 0);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
- notifications, null);
- facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
- payment.setStatus("PAID");
- facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
- verify(notifications, times(1)).paymentCaptured(order);
- }
- @Test
- void alreadyRefundedRetrieveDoesNotNotifyOrderAsNewlyPaid() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderMapper orderMapper = mock(PosOrderMapper.class);
- LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
- PosOrderLinePayment payment = payment("WAITING_AUTH");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
- any(), any())).thenReturn(1);
- PosOrder order = order(1L);
- when(orders.getOne(any(Wrapper.class))).thenReturn(order);
- when(orderMapper.markLinePaid("DD-1", 70L)).thenReturn(1);
- when(orderMapper.markLineRefunded("DD-1", 70L)).thenReturn(1);
- PosOrderLineRefund refund = new PosOrderLineRefund();
- refund.setId(80L);
- refund.setPaymentId(70L);
- refund.setStatus("CREATED");
- refund.setVersion(1L);
- when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(refund);
- when(refunds.markRefundedFromEvidence(eq(80L), eq(1L), eq("CREATED"),
- eq("RF-1"), any())).thenReturn(1);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
- notifications, null);
- facts.applyPaidAndRefundedFact(70L, "TX-1", "TSP", new Date(), "RF-1", new Date());
- verify(notifications, never()).paymentCaptured(any());
- }
- @Test
- void verifiedFullRefundEvidenceEscalatesPriorFailedRefund() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderMapper orderMapper = mock(PosOrderMapper.class);
- PosOrderLinePayment payment = payment("PAID");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("PAID"), eq("TX-1"),
- any(), any())).thenReturn(1);
- PosOrder order = order(1L);
- order.setPayStatus(1L);
- when(orders.getOne(any(Wrapper.class))).thenReturn(order);
- when(orderMapper.markLineRefunded("DD-1", 70L)).thenReturn(1);
- PosOrderLineRefund failed = new PosOrderLineRefund();
- failed.setId(80L);
- failed.setPaymentId(70L);
- failed.setStatus("FAILED");
- failed.setVersion(3L);
- when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(failed);
- when(refunds.markRefundedFromEvidence(eq(80L), eq(3L), eq("FAILED"),
- eq("RF-EXTERNAL"), any())).thenReturn(1);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
- mock(LinePayOrderNotificationService.class), null);
- facts.applyPaidAndRefundedFact(70L, "TX-1", "TSP", new Date(),
- "RF-EXTERNAL", new Date());
- verify(refunds).markRefundedFromEvidence(eq(80L), eq(3L), eq("FAILED"),
- eq("RF-EXTERNAL"), any());
- verify(refunds, never()).markRefunded(any(), any(), any(), any());
- }
- @Test
- void partialRefundEvidenceRecordsPaymentFactWithoutFulfillingOrder() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderMapper orderMapper = mock(PosOrderMapper.class);
- LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
- PosOrderLinePayment payment = payment("WAITING_AUTH");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
- any(), any())).thenReturn(1);
- PosOrderLineRefund refund = new PosOrderLineRefund();
- refund.setId(80L);
- refund.setPaymentId(70L);
- refund.setStatus("CREATED");
- refund.setVersion(1L);
- when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(refund);
- when(refunds.markManualReviewFromEvidence(80L, 1L, "CREATED")).thenReturn(1);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
- notifications, null);
- facts.applyPaidWithRefundReviewFact(70L, "TX-1", "TSP", new Date());
- verify(orderMapper, never()).markLinePaid(any(), any());
- verify(notifications, never()).paymentCaptured(any());
- verify(refunds).markManualReviewFromEvidence(80L, 1L, "CREATED");
- }
- @Test
- void ambiguousLaterEvidenceNeverDowngradesConfirmedRefund() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderLinePayment payment = payment("PAID");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("PAID"), eq("TX-1"),
- any(), any())).thenReturn(1);
- PosOrderLineRefund refunded = new PosOrderLineRefund();
- refunded.setId(80L);
- refunded.setPaymentId(70L);
- refunded.setStatus("REFUNDED");
- refunded.setVersion(3L);
- when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(refunded);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
- facts.applyPaidWithRefundReviewFact(70L, "TX-1", "TSP", new Date());
- verify(refunds, never()).markManualReview(any(), any(), any());
- }
- @Test
- void verifiedPartialRefundEvidenceEscalatesPriorFailedRefund() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderLinePayment payment = payment("PAID");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("PAID"), eq("TX-1"),
- any(), any())).thenReturn(1);
- PosOrderLineRefund failed = new PosOrderLineRefund();
- failed.setId(80L);
- failed.setPaymentId(70L);
- failed.setStatus("FAILED");
- failed.setVersion(3L);
- when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(failed);
- when(refunds.markManualReviewFromEvidence(80L, 3L, "FAILED")).thenReturn(1);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
- facts.applyPaidWithRefundReviewFact(70L, "TX-1", "TSP", new Date());
- verify(refunds).markManualReviewFromEvidence(80L, 3L, "FAILED");
- verify(refunds, never()).markManualReview(any(), any(), any());
- }
- @Test
- void cancelledLateCaptureCanCommitFullRefundWithoutPaymentNotification() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderMapper orderMapper = mock(PosOrderMapper.class);
- LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
- OrderLifecycleService lifecycle = mock(OrderLifecycleService.class);
- PosOrderLinePayment payment = payment("CONFIRM_UNKNOWN");
- when(payments.getById(70L)).thenReturn(payment);
- when(payments.markPaid(eq(70L), eq(2L), eq("CONFIRM_UNKNOWN"), eq("TX-1"),
- any(), any())).thenReturn(1);
- PosOrder cancelled = order(4L);
- when(orders.getOne(any(Wrapper.class))).thenReturn(cancelled);
- when(orderMapper.markLinePaid("DD-1", 70L)).thenAnswer(invocation -> {
- cancelled.setPayStatus(1L);
- return 1;
- });
- PosOrderLineRefund refund = new PosOrderLineRefund();
- refund.setId(80L);
- refund.setPaymentId(70L);
- refund.setStatus("CREATED");
- refund.setVersion(1L);
- when(refunds.createIfAbsent(payment, "TASK")).thenReturn(refund);
- when(refunds.markRefunded(eq(80L), eq(1L), eq("RF-1"), any())).thenReturn(1);
- when(orderMapper.markLineRefunded("DD-1", 70L)).thenAnswer(invocation -> {
- cancelled.setPayStatus(2L);
- return 1;
- });
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
- notifications, lifecycle);
- facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
- facts.applyRefundedFact(80L, 70L, 1L, "RF-1", new Date());
- assertEquals(2L, cancelled.getPayStatus());
- verify(notifications, never()).paymentCaptured(any());
- verify(lifecycle).finalizeSystemLineRefund(9L);
- }
- @Test
- void amountMismatchFactCreatesRefundIntentOnlyAfterEvidenceCas() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- PosOrderLinePayment persisted = payment("AMOUNT_MISMATCH");
- persisted.setCapturedAmount(100);
- when(payments.markOfflineEvidence(eq(70L), eq(2L), eq("REQUESTING"),
- eq("AMOUNT_MISMATCH"), eq("TX-1"), eq("TSP"), eq(100), any(Date.class)))
- .thenReturn(1);
- when(payments.getById(70L)).thenReturn(persisted);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
- assertEquals(true, facts.applyAmountMismatchFact(70L, 2L, "REQUESTING",
- "TX-1", "TSP", 100, new Date()));
- verify(refunds).createIfAbsent(persisted, "AMOUNT_MISMATCH");
- }
- @Test
- void failedAmountMismatchEvidenceCasNeverCreatesRefundIntent() {
- IPosOrderService orders = mock(IPosOrderService.class);
- IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
- IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
- when(payments.markOfflineEvidence(eq(70L), eq(2L), eq("REQUESTING"),
- eq("AMOUNT_MISMATCH"), eq("TX-1"), eq("TSP"), eq(100), any(Date.class)))
- .thenReturn(0);
- LinePayFactService facts = new LinePayFactService(orders, payments, refunds);
- assertEquals(false, facts.applyAmountMismatchFact(70L, 2L, "REQUESTING",
- "TX-1", "TSP", 100, new Date()));
- verify(refunds, never()).createIfAbsent(any(), any());
- }
- private static PosOrderLinePayment payment(String status) {
- PosOrderLinePayment payment = new PosOrderLinePayment();
- payment.setId(70L);
- payment.setDdId("DD-1");
- payment.setTransactionId("TX-1");
- payment.setStatus(status);
- payment.setVersion(2L);
- return payment;
- }
- private static PosOrder order(Long state) {
- PosOrder order = new PosOrder();
- order.setId(9L);
- order.setDdId("DD-1");
- order.setState(state);
- order.setPayType("3");
- order.setPayStatus(0L);
- order.setAfterSaleStatus(0L);
- return order;
- }
- }
|