package com.ruoyi.app.order; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.ruoyi.app.order.dto.AdminOrderActionRequest; import com.ruoyi.app.order.dto.AdminOrderStatusContext; import com.ruoyi.app.order.dto.AdminOrderStatusUpdateRequest; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.system.domain.PosOrder; import com.ruoyi.system.domain.PosOrderOmgPayment; import com.ruoyi.system.domain.PosOrderOmgRefund; import com.ruoyi.system.service.IPosOrderOmgPaymentService; import com.ruoyi.system.service.IPosOrderOmgRefundService; import com.ruoyi.system.service.IPosOrderService; import com.ruoyi.system.service.IPointsTransactionService; import com.ruoyi.system.service.IUserWalletService; import com.ruoyi.system.utils.OrderLogHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; 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 OrderLifecycleServiceTest { private IPosOrderService posOrderService; private OrderService billingService; private OrderLogHelper orderLogHelper; private IUserWalletService userWalletService; private IPointsTransactionService pointsTransactionService; private IPosOrderOmgPaymentService omgPaymentService; private IPosOrderOmgRefundService omgRefundService; private OrderLifecycleService service; @BeforeEach void setUp() { posOrderService = mock(IPosOrderService.class); billingService = mock(OrderService.class); orderLogHelper = mock(OrderLogHelper.class); userWalletService = mock(IUserWalletService.class); pointsTransactionService = mock(IPointsTransactionService.class); omgPaymentService = mock(IPosOrderOmgPaymentService.class); omgRefundService = mock(IPosOrderOmgRefundService.class); service = new OrderLifecycleService(posOrderService, billingService, orderLogHelper, userWalletService, pointsTransactionService, omgPaymentService, omgRefundService); } @Test void exposesOnlyLegalNextOrderAndDeliveryStates() { PosOrder order = deliveryOrder(); when(posOrderService.getById(10L)).thenReturn(order); PosOrderOmgPayment payment = new PosOrderOmgPayment(); payment.setId(1L); payment.setPayStatus(1); when(omgPaymentService.getLatestByDdId(order.getDdId())).thenReturn(payment); when(omgRefundService.listByPayment(1L)).thenReturn(List.of()); AdminOrderStatusContext context = service.getStatusContext(10L); assertEquals(List.of(2L), context.getAllowedOrderStates()); assertEquals(List.of(2L, 3L), context.getAllowedDeliveryStatuses()); assertTrue(context.getCanRefundOmg()); assertFalse(context.getCanConfirmOfflinePayment()); } @Test void marksDeliveryAsDeliveredAndCompletesExactlyOnce() { PosOrder order = deliveryOrder(); when(posOrderService.getById(10L)).thenReturn(order); doAnswer(invocation -> { PosOrder update = invocation.getArgument(0); order.setState(update.getState()); order.setDeliveryStatus(update.getDeliveryStatus()); order.setPayStatus(update.getPayStatus()); order.setAfterSaleStatus(update.getAfterSaleStatus()); order.setSdTime(update.getSdTime()); return true; }).when(posOrderService).update(any(PosOrder.class), any(Wrapper.class)); AdminOrderStatusUpdateRequest request = statusRequest(2L, 2L, 1L, 0L); request.setTargetDeliveryStatus(3L); request.setReason("骑手端漏操作"); AdminOrderStatusContext context = service.updateStatus(10L, request, 99L, "平台管理员"); assertEquals(3L, context.getState()); assertEquals(3L, context.getDeliveryStatus()); assertNotNull(context.getSdTime()); verify(billingService).setSanghuBilling(order); verify(billingService).setQishouBilling(order); verify(orderLogHelper).logSync(anyString(), anyInt(), anyLong(), anyString(), anyString()); } @Test void staleSnapshotHasNoSideEffects() { PosOrder order = deliveryOrder(); order.setPayStatus(0L); when(posOrderService.getById(10L)).thenReturn(order); AdminOrderStatusUpdateRequest request = statusRequest(2L, 2L, 1L, 0L); request.setTargetDeliveryStatus(3L); request.setReason("陈旧页面提交"); ServiceException error = assertThrows(ServiceException.class, () -> service.updateStatus(10L, request, 99L, "平台管理员")); assertTrue(error.getMessage().contains("状态已变化")); verify(posOrderService, never()).update(any(PosOrder.class), any(Wrapper.class)); verify(billingService, never()).setSanghuBilling(any()); verify(orderLogHelper, never()).logSync(anyString(), anyInt(), any(), anyString(), anyString()); } @Test void casFailureAfterReadHasNoSideEffects() { PosOrder order = deliveryOrder(); when(posOrderService.getById(10L)).thenReturn(order); when(posOrderService.update(any(PosOrder.class), any(Wrapper.class))).thenReturn(false); AdminOrderStatusUpdateRequest request = statusRequest(2L, 2L, 1L, 0L); request.setTargetDeliveryStatus(3L); request.setReason("并发提交"); ServiceException error = assertThrows(ServiceException.class, () -> service.updateStatus(10L, request, 99L, "平台管理员")); assertTrue(error.getMessage().contains("状态已变化")); verify(billingService, never()).setSanghuBilling(any()); verify(orderLogHelper, never()).logSync(anyString(), anyInt(), any(), anyString(), anyString()); } @Test void rejectsDeliveryRollback() { PosOrder order = deliveryOrder(); when(posOrderService.getById(10L)).thenReturn(order); AdminOrderStatusUpdateRequest request = statusRequest(2L, 2L, 1L, 0L); request.setTargetDeliveryStatus(1L); request.setReason("错误回退"); assertThrows(ServiceException.class, () -> service.updateStatus(10L, request, 99L, "平台管理员")); verify(posOrderService, never()).update(any(PosOrder.class), any(Wrapper.class)); } @Test void confirmsOnlyOfflinePayment() { PosOrder order = pickupOrder("1", 0L); when(posOrderService.getById(20L)).thenReturn(order); doAnswer(invocation -> { PosOrder update = invocation.getArgument(0); order.setPayStatus(update.getPayStatus()); return true; }).when(posOrderService).update(any(PosOrder.class), any(Wrapper.class)); AdminOrderStatusContext context = service.confirmOfflinePayment(20L, actionRequest(2L, null, 0L, 0L, "门店已收现金"), 99L, "平台管理员"); assertEquals(1L, context.getPayStatus()); verify(orderLogHelper).logSync(anyString(), anyInt(), anyLong(), anyString(), anyString()); } @Test void rejectsOmgOrderFromOfflinePaymentPath() { PosOrder order = pickupOrder("7", 0L); when(posOrderService.getById(20L)).thenReturn(order); assertThrows(ServiceException.class, () -> service.confirmOfflinePayment(20L, actionRequest(2L, null, 0L, 0L, "不能伪造在线支付"), 99L, "平台管理员")); verify(posOrderService, never()).update(any(PosOrder.class), any(Wrapper.class)); } @Test void offlineRefundSynchronizesAllBusinessStates() { PosOrder order = pickupOrder("1", 1L); order.setState(1L); when(posOrderService.getById(20L)).thenReturn(order); doAnswer(invocation -> { PosOrder update = invocation.getArgument(0); order.setState(update.getState()); order.setPayStatus(update.getPayStatus()); order.setAfterSaleStatus(update.getAfterSaleStatus()); return true; }).when(posOrderService).update(any(PosOrder.class), any(Wrapper.class)); AdminOrderStatusContext context = service.confirmOfflineRefund(20L, actionRequest(1L, null, 1L, 0L, "已现场退还现金"), 99L, "平台管理员"); assertEquals(4L, context.getState()); assertEquals(2L, context.getPayStatus()); assertEquals(3L, context.getAfterSaleStatus()); } @Test void completedOrderCannotBeRefunded() { PosOrder order = pickupOrder("1", 1L); order.setState(3L); when(posOrderService.getById(20L)).thenReturn(order); assertThrows(ServiceException.class, () -> service.confirmOfflineRefund(20L, actionRequest(3L, null, 1L, 0L, "申请退款"), 99L, "平台管理员")); verify(posOrderService, never()).update(any(PosOrder.class), any(Wrapper.class)); } @Test void blankReasonIsRejectedBeforeAnyWrite() { PosOrder order = pickupOrder("1", 0L); when(posOrderService.getById(20L)).thenReturn(order); assertThrows(ServiceException.class, () -> service.confirmOfflinePayment(20L, actionRequest(2L, null, 0L, 0L, " "), 99L, "平台管理员")); verify(posOrderService, never()).update(any(PosOrder.class), any(Wrapper.class)); } @Test void riderDeliveryCompletesCashOrderAndPreservesInvariant() { PosOrder order = deliveryOrder(); order.setPayType("1"); order.setCollectPayment("1"); order.setPayStatus(0L); when(posOrderService.getById(10L)).thenReturn(order); doAnswer(invocation -> { PosOrder update = invocation.getArgument(0); order.setState(update.getState()); order.setDeliveryStatus(update.getDeliveryStatus()); order.setPayStatus(update.getPayStatus()); order.setSdTime(update.getSdTime()); return true; }).when(posOrderService).update(any(PosOrder.class), any(Wrapper.class)); AdminOrderStatusContext context = service.completeDeliveryByRider(10L, 88L, "骑手甲"); assertEquals(3L, context.getState()); assertEquals(3L, context.getDeliveryStatus()); assertEquals(1L, context.getPayStatus()); verify(billingService).setSanghuBilling(order); verify(billingService).setQishouBilling(order); } @Test void synchronizesSystemOmgRefundToAllThreeBusinessStates() { PosOrder order = pickupOrder("7", 1L); order.setState(1L); when(posOrderService.getById(20L)).thenReturn(order); doAnswer(invocation -> { PosOrder update = invocation.getArgument(0); order.setState(update.getState()); order.setPayStatus(update.getPayStatus()); order.setAfterSaleStatus(update.getAfterSaleStatus()); return true; }).when(posOrderService).update(any(PosOrder.class), any(Wrapper.class)); AdminOrderStatusContext context = service.finalizeSystemOmgRefund(20L); assertEquals(4L, context.getState()); assertEquals(2L, context.getPayStatus()); assertEquals(3L, context.getAfterSaleStatus()); verify(orderLogHelper).logSync(anyString(), anyInt(), any(), anyString(), anyString()); } @Test void exposesManualRefundLocalCompensationAfterLedgerAlreadyCompleted() { PosOrder order = pickupOrder("7", 1L); order.setState(1L); PosOrderOmgPayment payment = new PosOrderOmgPayment(); payment.setId(3L); payment.setPayStatus(3); payment.setPayType("ATM_TAISHIN"); PosOrderOmgRefund completed = new PosOrderOmgRefund(); completed.setRtnCode(1); when(posOrderService.getById(20L)).thenReturn(order); when(omgPaymentService.getLatestByDdId(order.getDdId())).thenReturn(payment); when(omgRefundService.listByPayment(3L)).thenReturn(List.of(completed)); AdminOrderStatusContext context = service.getStatusContext(20L); assertTrue(context.getCanConfirmManualOmgRefund()); assertFalse(context.getCanRefundOmg()); assertFalse(context.getManualRefundPending()); } private PosOrder deliveryOrder() { PosOrder order = new PosOrder(); order.setId(10L); order.setDdId("202608100001"); order.setType(0L); order.setPayType("7"); order.setState(2L); order.setDeliveryStatus(2L); order.setPayStatus(1L); order.setAfterSaleStatus(0L); order.setQsId(88L); order.setShId(66L); order.setMdId(77L); order.setAmount(100); order.setFood("[]"); order.setFreight(10D); return order; } private PosOrder pickupOrder(String payType, Long payStatus) { PosOrder order = new PosOrder(); order.setId(20L); order.setDdId("202608100002"); order.setType(1L); order.setPayType(payType); order.setState(2L); order.setPayStatus(payStatus); order.setAfterSaleStatus(0L); return order; } private AdminOrderStatusUpdateRequest statusRequest(Long state, Long delivery, Long pay, Long afterSale) { AdminOrderStatusUpdateRequest request = new AdminOrderStatusUpdateRequest(); request.setExpectedState(state); request.setExpectedDeliveryStatus(delivery); request.setExpectedPayStatus(pay); request.setExpectedAfterSaleStatus(afterSale); return request; } private AdminOrderActionRequest actionRequest(Long state, Long delivery, Long pay, Long afterSale, String reason) { AdminOrderActionRequest request = new AdminOrderActionRequest(); request.setExpectedState(state); request.setExpectedDeliveryStatus(delivery); request.setExpectedPayStatus(pay); request.setExpectedAfterSaleStatus(afterSale); request.setReason(reason); return request; } }