| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- package com.ruoyi.app.pay;
- import com.alibaba.fastjson2.JSONObject;
- import com.baomidou.mybatisplus.core.conditions.Wrapper;
- import com.ruoyi.app.utils.linepay.LinePayClient;
- import com.ruoyi.app.utils.linepay.LinePayResponse;
- import com.ruoyi.system.domain.PosOrder;
- import com.ruoyi.system.domain.PosOrderLinePayment;
- import com.ruoyi.system.domain.PosOrderLineRefund;
- import com.ruoyi.system.domain.PosStoreLinePay;
- import com.ruoyi.system.service.IPosOrderLinePaymentService;
- import com.ruoyi.system.service.IPosOrderLineRefundService;
- import com.ruoyi.system.service.IPosOrderService;
- import com.ruoyi.system.service.IPosStoreLinePayService;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Test;
- 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;
- class LinePayReconcileTest {
- private IPosOrderService orderService;
- private IPosStoreLinePayService credentialService;
- private IPosOrderLinePaymentService paymentService;
- private IPosOrderLineRefundService refundService;
- private LinePayClient client;
- private LinePayFactService factService;
- private LinePayService service;
- @BeforeEach
- void setUp() {
- orderService = mock(IPosOrderService.class);
- credentialService = mock(IPosStoreLinePayService.class);
- paymentService = mock(IPosOrderLinePaymentService.class);
- refundService = mock(IPosOrderLineRefundService.class);
- client = mock(LinePayClient.class);
- factService = mock(LinePayFactService.class);
- service = new LinePayService(orderService, credentialService, paymentService,
- refundService, client, factService);
- when(paymentService.getById(70L)).thenReturn(payment("WAITING_AUTH"));
- when(credentialService.getById(5L)).thenReturn(credential());
- }
- @Test
- void retrieveUniqueCapturedPaymentAppliesPaidFactWithoutCheckOrConfirm() throws Exception {
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
- "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
- + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
- + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}]}]}"));
- String status = service.reconcilePayment(70L, "TASK");
- assertEquals("PAID", status);
- verify(factService).applyPaidFact(eq(70L), eq("TX-100"), any(), any());
- verify(client, never()).check(any(), any());
- verify(client, never()).confirm(any(), any(), any(Integer.class), any());
- }
- @Test
- void rotatedSameChannelCredentialMayRecoverOnlyAfterStrictRetrieveProof() throws Exception {
- PosStoreLinePay current = currentCredential("channel");
- when(credentialService.getCurrent(9L)).thenReturn(current);
- when(client.retrieveByTransactionId(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
- 5L, "channel", "secret")), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"1105\"}"));
- when(client.retrieveByTransactionId(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
- 6L, "channel", "new-secret")), eq("TX-100"))).thenReturn(response(
- "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
- + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
- + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}]}]}"));
- assertEquals("PAID", service.reconcilePayment(70L, "TASK"));
- verify(factService).applyPaidFact(eq(70L), eq("TX-100"), any(), any());
- }
- @Test
- void rotatedDifferentChannelCredentialIsNeverUsedForOldTransaction() throws Exception {
- when(credentialService.getCurrent(9L)).thenReturn(currentCredential("different-channel"));
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"1105\"}"));
- assertEquals("WAITING_AUTH", service.reconcilePayment(70L, "TASK"));
- verify(client, org.mockito.Mockito.times(1)).retrieveByTransactionId(any(), eq("TX-100"));
- verify(factService, never()).applyPaidFact(any(), any(), any(), any());
- }
- @Test
- void confirmedLocalRefundIsNeverDowngradedByLaterAmbiguousEvidence() throws Exception {
- PosOrderLineRefund refunded = new PosOrderLineRefund();
- refunded.setPaymentId(70L);
- refunded.setStatus("REFUNDED");
- when(refundService.getByPaymentId(70L)).thenReturn(refunded);
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
- "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
- + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
- + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}],"
- + "\"refundList\":[{\"refundTransactionId\":\"RF-PART\","
- + "\"refundAmount\":-100}]}]}"));
- assertEquals("REFUNDED", service.reconcilePayment(70L, "TASK"));
- verify(factService, never()).applyPaidWithRefundReviewFact(any(), any(), any(), any());
- }
- @Test
- void check0110ConfirmsOnlyAfterLocalClaimAndThenAppliesPaidFact() throws Exception {
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"1150\"}"));
- when(client.check(any(), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"0110\"}"));
- when(orderService.getOne(any(Wrapper.class))).thenReturn(order(0L));
- when(paymentService.claimConfirm(eq(70L), eq(2L), eq("WAITING_AUTH"),
- any(), any())).thenReturn(1);
- when(client.confirm(any(), eq("TX-100"), eq(280), eq("TWD"))).thenReturn(
- response("{\"returnCode\":\"0000\",\"info\":{\"transactionId\":\"TX-100\","
- + "\"orderId\":\"LP-100\",\"currency\":\"TWD\","
- + "\"payInfo\":[{\"amount\":280}]}}"));
- String status = service.reconcilePayment(70L, "TASK");
- assertEquals("PAID", status);
- verify(paymentService).claimConfirm(eq(70L), eq(2L), eq("WAITING_AUTH"), any(), any());
- verify(factService).applyPaidFact(eq(70L), eq("TX-100"), any(), any());
- }
- @Test
- void check0121TerminatesOnlyAfterRetrieveFoundNoPayment() throws Exception {
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"1150\"}"));
- when(client.check(any(), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"0121\"}"));
- when(paymentService.markTerminal(70L, 2L, "WAITING_AUTH", "CANCELLED_OR_EXPIRED"))
- .thenReturn(1);
- String status = service.reconcilePayment(70L, "TASK");
- assertEquals("CANCELLED_OR_EXPIRED", status);
- verify(paymentService).markTerminal(70L, 2L, "WAITING_AUTH", "CANCELLED_OR_EXPIRED");
- }
- @Test
- void cancelledOrderIsNeverConfirmedAfterAuthorization() throws Exception {
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"1150\"}"));
- when(client.check(any(), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"0110\"}"));
- when(orderService.getOne(any(Wrapper.class))).thenReturn(order(4L));
- when(paymentService.markAuthDoneOrderCancelled(eq(70L), eq(2L), eq("WAITING_AUTH"),
- any(), any()))
- .thenReturn(1);
- String status = service.reconcilePayment(70L, "TASK");
- assertEquals("AUTH_DONE_ORDER_CANCELLED", status);
- verify(client, never()).confirm(any(), any(), any(Integer.class), any());
- }
- @Test
- void requestUnknownCanRecoverCapturedTransactionByOrderId() throws Exception {
- PosOrderLinePayment unknown = payment("REQUEST_UNKNOWN");
- unknown.setTransactionId(null);
- when(paymentService.getById(70L)).thenReturn(unknown);
- when(client.retrieveByOrderId(any(), eq("LP-100"))).thenReturn(response(
- "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-RECOVERED\","
- + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
- + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}]}]}"));
- String status = service.reconcilePayment(70L, "TASK");
- assertEquals("PAID", status);
- verify(factService).applyPaidFact(eq(70L), eq("TX-RECOVERED"), any(), any());
- }
- @Test
- void check0123ImmediatelyRetrievesAgainBeforeDeciding() throws Exception {
- when(client.retrieveByTransactionId(any(), eq("TX-100")))
- .thenReturn(response("{\"returnCode\":\"1150\"}"))
- .thenReturn(response("{\"returnCode\":\"0000\",\"info\":[{"
- + "\"transactionId\":\"TX-100\",\"orderId\":\"LP-100\","
- + "\"transactionType\":\"PAYMENT\",\"currency\":\"TWD\","
- + "\"payInfo\":[{\"amount\":280}]}]}"));
- when(client.check(any(), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"0123\"}"));
- String status = service.reconcilePayment(70L, "TASK");
- assertEquals("PAID", status);
- verify(factService).applyPaidFact(eq(70L), eq("TX-100"), any(), any());
- }
- @Test
- void expiredRecoveryPerformsFinalRetrieveThenStopsForManualReview() throws Exception {
- PosOrderLinePayment expired = payment("CONFIRM_UNKNOWN");
- expired.setReconcileDeadline(new java.util.Date(1L));
- when(paymentService.getById(70L)).thenReturn(expired);
- when(client.retrieveByTransactionId(any(), eq("TX-100")))
- .thenThrow(new IllegalStateException("timeout"));
- when(paymentService.markManualReview(70L, 2L, "CONFIRM_UNKNOWN")).thenReturn(1);
- assertEquals("MANUAL_REVIEW", service.reconcilePayment(70L, "TASK"));
- verify(paymentService).markManualReview(70L, 2L, "CONFIRM_UNKNOWN");
- }
- @Test
- void confirmUnknownNeverChecksOrConfirmsAgain() throws Exception {
- PosOrderLinePayment unknown = payment("CONFIRM_UNKNOWN");
- when(paymentService.getById(70L)).thenReturn(unknown);
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
- response("{\"returnCode\":\"1150\"}"));
- assertEquals("CONFIRM_UNKNOWN", service.reconcilePayment(70L, "TASK"));
- verify(client, never()).check(any(), any());
- verify(client, never()).confirm(any(), any(), any(Integer.class), any());
- }
- @Test
- void retrieveAmountMismatchDoesNotApplyPaidFact() throws Exception {
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
- "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
- + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
- + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":100}]}]}"));
- assertEquals("WAITING_AUTH", service.reconcilePayment(70L, "TASK"));
- verify(factService, never()).applyPaidFact(any(), any(), any(), any());
- verify(client, never()).check(any(), any());
- }
- @Test
- void retrieveAlreadyFullyRefundedPaymentAppliesBothFacts() throws Exception {
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
- "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
- + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
- + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}],"
- + "\"refundList\":[{\"refundTransactionId\":\"RF-1\","
- + "\"transactionType\":\"PARTIAL_REFUND\",\"refundAmount\":-280}]}]}"));
- assertEquals("REFUNDED", service.reconcilePayment(70L, "TASK"));
- verify(factService).applyPaidAndRefundedFact(eq(70L), eq("TX-100"), any(), any(),
- eq("RF-1"), any());
- verify(factService, never()).applyPaidFact(any(), any(), any(), any());
- }
- @Test
- void retrievePartiallyRefundedPaymentStopsForManualReviewWithoutFulfillment() throws Exception {
- when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
- "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
- + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
- + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}],"
- + "\"refundList\":[{\"refundTransactionId\":\"RF-PART\","
- + "\"refundAmount\":-100}]}]}"));
- assertEquals("MANUAL_REVIEW", service.reconcilePayment(70L, "TASK"));
- verify(factService).applyPaidWithRefundReviewFact(eq(70L), eq("TX-100"), any(), any());
- verify(factService, never()).applyPaidFact(any(), any(), any(), any());
- }
- private static LinePayResponse response(String raw) {
- JSONObject root = JSONObject.parseObject(raw);
- return new LinePayResponse(200, root.getString("returnCode"),
- root.getString("returnMessage"), null, root.getJSONObject("info"), raw);
- }
- private static PosOrderLinePayment payment(String status) {
- PosOrderLinePayment payment = new PosOrderLinePayment();
- payment.setId(70L);
- payment.setDdId("DD-100");
- payment.setLineOrderId("LP-100");
- payment.setTransactionId("TX-100");
- payment.setCredentialId(5L);
- payment.setStoreId(9L);
- payment.setAmount(280);
- payment.setCurrency("TWD");
- payment.setStatus(status);
- payment.setActiveDdId("DD-100");
- payment.setVersion(2L);
- return payment;
- }
- private static PosStoreLinePay credential() {
- PosStoreLinePay value = new PosStoreLinePay();
- value.setId(5L);
- value.setEnvironment("SANDBOX");
- value.setChannelId("channel");
- value.setChannelSecret("secret");
- return value;
- }
- private static PosStoreLinePay currentCredential(String channelId) {
- PosStoreLinePay value = new PosStoreLinePay();
- value.setId(6L);
- value.setStoreId(9L);
- value.setEnvironment("SANDBOX");
- value.setChannelId(channelId);
- value.setChannelSecret("new-secret");
- return value;
- }
- private static PosOrder order(Long state) {
- PosOrder order = new PosOrder();
- order.setDdId("DD-100");
- order.setMdId(9L);
- order.setAmount(280);
- order.setPayType("3");
- order.setPayStatus(0L);
- order.setState(state);
- return order;
- }
- }
|