fix: correct order lifecycle refunds and add coverage
Some checks failed
Build and Push Backend / docker (push) Failing after 5s

This commit is contained in:
irving
2025-10-27 00:12:07 -04:00
parent f7461abc83
commit 6b2a1c2ba7
11 changed files with 816 additions and 130 deletions

View File

@@ -11,6 +11,7 @@ import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
import com.starry.admin.modules.order.module.dto.CommodityInfo;
import com.starry.admin.modules.order.module.dto.OrderCreationRequest;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
@@ -450,7 +451,7 @@ class PlayOrderInfoServiceTest {
String orderId = "order_force_cancel";
PlayOrderInfoEntity inProgressOrder = new PlayOrderInfoEntity();
inProgressOrder.setId(orderId);
inProgressOrder.setOrderStatus(OrderConstant.ORDER_STATUS_2);
inProgressOrder.setOrderStatus(OrderStatus.IN_PROGRESS.getCode());
inProgressOrder.setAcceptBy("clerk-1");
inProgressOrder.setPurchaserBy("customer-1");
inProgressOrder.setFinalAmount(BigDecimal.valueOf(100));
@@ -458,7 +459,7 @@ class PlayOrderInfoServiceTest {
PlayOrderInfoEntity cancelledOrder = new PlayOrderInfoEntity();
cancelledOrder.setId(orderId);
cancelledOrder.setOrderStatus(OrderConstant.ORDER_STATUS_4);
cancelledOrder.setOrderStatus(OrderStatus.CANCELLED.getCode());
PlayCustomUserInfoEntity customUserInfo = new PlayCustomUserInfoEntity();
customUserInfo.setId("customer-1");
@@ -472,18 +473,22 @@ class PlayOrderInfoServiceTest {
any(BigDecimal.class), anyString(), anyString(), any(BigDecimal.class), any(BigDecimal.class), eq(orderId));
doNothing().when(playOrderRefundInfoService).add(eq(orderId), eq("customer-1"), eq("clerk-1"), anyString(),
anyString(), any(BigDecimal.class), anyString(), anyString(), anyString(), anyString(), anyString());
doNothing().when(wxCustomMpService).sendOrderCancelMessage(any(PlayOrderInfoEntity.class), anyString());
doNothing().when(wxCustomMpService).sendOrderCancelMessageAsync(any(PlayOrderInfoEntity.class), anyString());
assertDoesNotThrow(() -> orderService.forceCancelOngoingOrder("2", "admin-1", orderId,
BigDecimal.valueOf(80), "管理员取消测试", Collections.emptyList()));
verify(orderInfoMapper, times(1)).updateById(any(PlayOrderInfoEntity.class));
verify(customUserInfoService, times(1)).updateAccountBalanceById(eq("customer-1"), any(BigDecimal.class),
any(BigDecimal.class), anyString(), anyString(), eq(BigDecimal.valueOf(80)), eq(BigDecimal.ZERO),
eq(orderId));
verify(playOrderRefundInfoService, times(1)).add(eq(orderId), eq("customer-1"), eq("clerk-1"), anyString(),
eq("0"), eq(BigDecimal.valueOf(80)), eq("管理员取消测试"), eq("2"), eq("admin-1"), eq("0"), eq("0"));
verify(wxCustomMpService, times(1)).sendOrderCancelMessage(any(PlayOrderInfoEntity.class),
any(BigDecimal.class), eq(OrderConstant.BalanceOperationType.REFUND.getCode()), eq("订单取消退款"),
eq(BigDecimal.valueOf(80)), eq(BigDecimal.ZERO), eq(orderId));
verify(playOrderRefundInfoService, times(1)).add(eq(orderId), eq("customer-1"), eq("clerk-1"),
eq(inProgressOrder.getPayMethod()),
eq(OrderConstant.OrderRefundRecordType.PARTIAL.getCode()),
eq(BigDecimal.valueOf(80)), eq("管理员取消测试"), eq("2"), eq("admin-1"),
eq(OrderConstant.OrderRefundState.PROCESSING.getCode()),
eq(OrderConstant.ReviewRequirement.NOT_REQUIRED.getCode()));
verify(wxCustomMpService, times(1)).sendOrderCancelMessageAsync(any(PlayOrderInfoEntity.class),
eq("管理员取消测试"));
}
@@ -493,7 +498,7 @@ class PlayOrderInfoServiceTest {
String orderId = "order_invalid_force_cancel";
PlayOrderInfoEntity pendingOrder = new PlayOrderInfoEntity();
pendingOrder.setId(orderId);
pendingOrder.setOrderStatus(OrderConstant.ORDER_STATUS_0);
pendingOrder.setOrderStatus(OrderStatus.PENDING.getCode());
pendingOrder.setAcceptBy("clerk-1");
pendingOrder.setPurchaserBy("customer-1");
pendingOrder.setFinalAmount(BigDecimal.valueOf(50));

View File

@@ -0,0 +1,216 @@
package com.starry.admin.modules.order.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundFlag;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundRecordType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundState;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
import com.starry.admin.modules.order.module.constant.OrderConstant.ReviewRequirement;
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class OrderLifecycleServiceImplTest {
@InjectMocks
private OrderLifecycleServiceImpl lifecycleService;
@Mock
private PlayOrderInfoMapper orderInfoMapper;
@Mock
private IEarningsService earningsService;
@Mock
private WxCustomMpService wxCustomMpService;
@Mock
private IPlayOrderRefundInfoService orderRefundInfoService;
@Mock
private IPlayCustomUserInfoService customUserInfoService;
@Test
void completeOrder_inProgress_createsEarningsAndNotifies() {
String orderId = UUID.randomUUID().toString();
PlayOrderInfoEntity inProgress = buildOrder(orderId, OrderStatus.IN_PROGRESS.getCode());
inProgress.setOrderEndTime(null);
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
completed.setOrderEndTime(LocalDateTime.now());
when(orderInfoMapper.selectById(orderId)).thenReturn(inProgress, completed);
when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);
mockEarningsCounts(0L, 1L);
OrderCompletionContext context = OrderCompletionContext.of(
OperatorType.CLERK.getCode(),
inProgress.getAcceptBy(),
OrderTriggerSource.WX_CLERK);
lifecycleService.completeOrder(orderId, context);
verify(orderInfoMapper).updateById(argThat(entity ->
orderId.equals(entity.getId())
&& OrderStatus.COMPLETED.getCode().equals(entity.getOrderStatus())
&& entity.getOrderEndTime() != null));
verify(earningsService).createFromOrder(completed);
verify(wxCustomMpService).sendOrderFinishMessageAsync(completed);
}
@Test
void completeOrder_alreadyCompleted_skipsEarningsCreation() {
String orderId = UUID.randomUUID().toString();
PlayOrderInfoEntity alreadyCompleted = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
alreadyCompleted.setOrderEndTime(null);
PlayOrderInfoEntity completedWithEnd = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
completedWithEnd.setOrderEndTime(LocalDateTime.now());
when(orderInfoMapper.selectById(orderId)).thenReturn(alreadyCompleted, completedWithEnd);
when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);
mockEarningsCounts(1L);
lifecycleService.completeOrder(orderId, OrderCompletionContext.of(
OperatorType.ADMIN.getCode(),
"admin-1",
OrderTriggerSource.ADMIN_CONSOLE));
verify(earningsService, never()).createFromOrder(any());
verify(wxCustomMpService).sendOrderFinishMessageAsync(completedWithEnd);
}
@Test
void refundOrder_partialAmount_updatesLedgerAndRecords() {
String orderId = UUID.randomUUID().toString();
BigDecimal finalAmount = BigDecimal.valueOf(100);
BigDecimal refundAmount = BigDecimal.valueOf(40);
PlayOrderInfoEntity order = buildOrder(orderId, OrderStatus.ACCEPTED.getCode());
order.setFinalAmount(finalAmount);
order.setOrderMoney(finalAmount);
order.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
order.setPayMethod("1");
when(orderInfoMapper.selectById(orderId)).thenReturn(order);
when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);
PlayCustomUserInfoEntity customer = new PlayCustomUserInfoEntity();
customer.setId(order.getPurchaserBy());
customer.setAccountBalance(BigDecimal.valueOf(10));
when(customUserInfoService.getById(order.getPurchaserBy())).thenReturn(customer);
OrderRefundContext context = new OrderRefundContext();
context.setOrderId(orderId);
context.setRefundAmount(refundAmount);
context.setRefundReason("部分退款测试");
context.setOperatorType(OperatorType.ADMIN.getCode());
context.setOperatorId("admin-1");
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
lifecycleService.refundOrder(context);
ArgumentCaptor<PlayOrderInfoEntity> updateCaptor = ArgumentCaptor.forClass(PlayOrderInfoEntity.class);
verify(orderInfoMapper).updateById(updateCaptor.capture());
PlayOrderInfoEntity updated = updateCaptor.getValue();
assertEquals(OrderStatus.CANCELLED.getCode(), updated.getOrderStatus());
assertEquals(OrderRefundFlag.REFUNDED.getCode(), updated.getRefundType());
assertEquals(refundAmount, updated.getRefundAmount());
verify(customUserInfoService).updateAccountBalanceById(
eq(order.getPurchaserBy()),
eq(customer.getAccountBalance()),
eq(customer.getAccountBalance().add(refundAmount)),
eq(BalanceOperationType.REFUND.getCode()),
eq("订单退款"),
eq(refundAmount),
eq(BigDecimal.ZERO),
eq(orderId));
verify(orderRefundInfoService).add(
eq(orderId),
eq(order.getPurchaserBy()),
eq(order.getAcceptBy()),
eq(order.getPayMethod()),
eq(OrderRefundRecordType.PARTIAL.getCode()),
eq(refundAmount),
eq("部分退款测试"),
eq(OperatorType.ADMIN.getCode()),
eq("admin-1"),
eq(OrderRefundState.PROCESSING.getCode()),
eq(ReviewRequirement.NOT_REQUIRED.getCode()));
}
@Test
void refundOrder_throwsWhenAlreadyRefunded() {
String orderId = UUID.randomUUID().toString();
PlayOrderInfoEntity order = buildOrder(orderId, OrderStatus.ACCEPTED.getCode());
order.setFinalAmount(BigDecimal.TEN);
order.setOrderMoney(BigDecimal.TEN);
order.setRefundType(OrderRefundFlag.REFUNDED.getCode());
when(orderInfoMapper.selectById(orderId)).thenReturn(order);
OrderRefundContext context = new OrderRefundContext();
context.setOrderId(orderId);
context.setRefundAmount(BigDecimal.ONE);
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
assertThrows(CustomException.class, () -> lifecycleService.refundOrder(context));
verify(orderInfoMapper, never()).updateById(any());
verify(orderRefundInfoService, never()).add(anyString(), anyString(), anyString(), anyString(), anyString(), any(), anyString(), anyString(), anyString(), anyString(), anyString());
}
private PlayOrderInfoEntity buildOrder(String orderId, String status) {
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
entity.setId(orderId);
entity.setOrderStatus(status);
entity.setAcceptBy("clerk-1");
entity.setPurchaserBy("customer-1");
entity.setTenantId("tenant-1");
return entity;
}
private void mockEarningsCounts(long... counts) {
LambdaQueryChainWrapper<EarningsLineEntity> chain = Mockito.mock(LambdaQueryChainWrapper.class);
when(chain.eq(any(), any())).thenReturn(chain);
if (counts.length == 0) {
when(chain.count()).thenReturn(0L);
} else {
org.mockito.stubbing.OngoingStubbing<Long> stubbing = when(chain.count()).thenReturn(counts[0]);
for (int i = 1; i < counts.length; i++) {
stubbing = stubbing.thenReturn(counts[i]);
}
}
when(earningsService.lambdaQuery()).thenReturn(chain);
}
}