This commit is contained in:
irving
2025-11-10 23:42:00 -05:00
parent ed0edf584a
commit e616dd6a13
17 changed files with 655 additions and 3 deletions

View File

@@ -33,6 +33,7 @@ import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {

View File

@@ -0,0 +1,80 @@
package com.starry.admin.modules.order.listener;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
import com.starry.admin.modules.order.module.dto.OrderRevocationContext.EarningsAdjustStrategy;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import java.math.BigDecimal;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class OrderRevocationEarningsListenerTest {
@Mock
private IEarningsService earningsService;
private OrderRevocationEarningsListener listener;
@BeforeEach
void setUp() {
listener = new OrderRevocationEarningsListener(earningsService);
}
@Test
void handle_counterStrategyFallsBackToEstimatedRevenueWhenRefundAmountZero() {
OrderRevocationContext context = new OrderRevocationContext();
context.setOrderId("order-counter-1");
context.setOperatorId("admin-op");
context.setEarningsStrategy(EarningsAdjustStrategy.COUNTER_TO_PEIPEI);
context.setRefundAmount(BigDecimal.ZERO);
context.setCounterClerkId("ops-clerk");
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
order.setId("order-counter-1");
order.setTenantId("tenant-77");
order.setEstimatedRevenue(BigDecimal.valueOf(68));
listener.handle(new OrderRevocationEvent(context, order));
verify(earningsService)
.createCounterLine(order.getId(), order.getTenantId(), "ops-clerk", BigDecimal.valueOf(68), "admin-op");
}
@Test
void handle_reverseStrategyRevertsAvailableLines() {
OrderRevocationContext context = new OrderRevocationContext();
context.setOrderId("order-reverse-2");
context.setOperatorId("admin-reviewer");
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
order.setId("order-reverse-2");
listener.handle(new OrderRevocationEvent(context, order));
verify(earningsService).reverseByOrder("order-reverse-2", "admin-reviewer");
}
@Test
void handle_noneStrategyIsNoOp() {
OrderRevocationContext context = new OrderRevocationContext();
context.setOrderId("order-none-3");
context.setOperatorId("admin-noop");
context.setEarningsStrategy(EarningsAdjustStrategy.NONE);
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
order.setId("order-none-3");
listener.handle(new OrderRevocationEvent(context, order));
verifyNoInteractions(earningsService);
}
}

View File

@@ -40,9 +40,12 @@ import com.starry.admin.modules.order.module.dto.OrderCreationContext;
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
import com.starry.admin.modules.order.module.dto.OrderRevocationContext.EarningsAdjustStrategy;
import com.starry.admin.modules.order.module.dto.PaymentInfo;
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
@@ -65,10 +68,12 @@ import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
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;
import org.springframework.context.ApplicationEventPublisher;
@ExtendWith(MockitoExtension.class)
class OrderLifecycleServiceImplTest {
@@ -106,11 +111,104 @@ class OrderLifecycleServiceImplTest {
@Mock
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
@Mock
private ApplicationEventPublisher applicationEventPublisher;
@BeforeEach
void initStrategies() {
lifecycleService.initPlacementStrategies();
}
@Test
void revokeCompletedOrder_updatesStatusAndPublishesEvent() {
String orderId = UUID.randomUUID().toString();
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
completed.setFinalAmount(BigDecimal.valueOf(188));
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
revoked.setFinalAmount(BigDecimal.valueOf(188));
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
when(earningsService.hasLockedLines(orderId)).thenReturn(false);
PlayCustomUserInfoEntity customer = buildCustomer(completed.getPurchaserBy(), BigDecimal.ZERO);
when(customUserInfoService.getById(completed.getPurchaserBy())).thenReturn(customer);
doNothing().when(customUserInfoService)
.updateAccountBalanceById(anyString(), any(), any(), anyString(), anyString(), any(), any(), anyString());
doNothing().when(orderRefundInfoService)
.add(anyString(), anyString(), anyString(), anyString(), anyString(), any(), anyString(), anyString(), anyString(), anyString(), anyString());
OrderRevocationContext context = new OrderRevocationContext();
context.setOrderId(orderId);
context.setOperatorId("admin-8");
context.setOperatorType(OperatorType.ADMIN.getCode());
context.setRefundAmount(BigDecimal.valueOf(88));
context.setRefundReason("客户投诉");
context.setRefundToCustomer(true);
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
lifecycleService.revokeCompletedOrder(context);
verify(orderInfoMapper).update(isNull(), any());
verify(orderLogInfoMapper).insert(any());
ArgumentCaptor<OrderRevocationEvent> captor = ArgumentCaptor.forClass(OrderRevocationEvent.class);
verify(applicationEventPublisher).publishEvent(captor.capture());
OrderRevocationEvent event = captor.getValue();
assertEquals(orderId, event.getContext().getOrderId());
assertEquals(EarningsAdjustStrategy.REVERSE_CLERK, event.getContext().getEarningsStrategy());
assertEquals(BigDecimal.valueOf(88), event.getContext().getRefundAmount());
}
@Test
void revokeCompletedOrder_defersBalanceCreditToListener() {
String orderId = UUID.randomUUID().toString();
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
completed.setFinalAmount(BigDecimal.valueOf(208));
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
revoked.setFinalAmount(BigDecimal.valueOf(208));
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
when(earningsService.hasLockedLines(orderId)).thenReturn(false);
when(customUserInfoService.getById(anyString())).thenThrow(new AssertionError("Balance update should be handled by listener"));
OrderRevocationContext context = new OrderRevocationContext();
context.setOrderId(orderId);
context.setOperatorId("admin-9");
context.setOperatorType(OperatorType.ADMIN.getCode());
context.setRefundAmount(BigDecimal.valueOf(108));
context.setRefundReason("质量问题");
context.setRefundToCustomer(true);
context.setEarningsStrategy(EarningsAdjustStrategy.NONE);
lifecycleService.revokeCompletedOrder(context);
verify(customUserInfoService, never()).getById(anyString());
verify(applicationEventPublisher).publishEvent(any(OrderRevocationEvent.class));
}
@Test
void revokeCompletedOrder_blocksWhenEarningsLocked() {
String orderId = UUID.randomUUID().toString();
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
when(orderInfoMapper.selectById(orderId)).thenReturn(completed);
when(earningsService.hasLockedLines(orderId)).thenReturn(true);
OrderRevocationContext context = new OrderRevocationContext();
context.setOrderId(orderId);
context.setOperatorId("admin-locked");
context.setOperatorType(OperatorType.ADMIN.getCode());
context.setRefundToCustomer(false);
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
verify(orderInfoMapper, never()).update(isNull(), any());
verify(applicationEventPublisher, never()).publishEvent(any());
}
@Test
void placeOrder_throwsWhenCommandNull() {
assertThrows(CustomException.class, () -> lifecycleService.placeOrder(null));
@@ -1223,6 +1321,13 @@ class OrderLifecycleServiceImplTest {
return entity;
}
private PlayCustomUserInfoEntity buildCustomer(String id, BigDecimal balance) {
PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity();
entity.setId(id);
entity.setAccountBalance(balance);
return entity;
}
private void stubDefaultPersistence() {
lenient().when(orderInfoMapper.selectCount(any())).thenReturn(0L);
lenient().when(orderInfoMapper.insert(any())).thenReturn(1);

View File

@@ -12,6 +12,8 @@ import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -68,4 +70,54 @@ class EarningsServiceImplTest {
verify(baseMapper, never()).insert(any());
}
@Test
void createCounterLine_persistsNegativeAvailableLine() {
when(baseMapper.insert(any())).thenReturn(1);
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.valueOf(88), "admin");
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
verify(baseMapper).insert(captor.capture());
EarningsLineEntity saved = captor.getValue();
assertEquals(new BigDecimal("-88"), saved.getAmount());
assertEquals("clerk-c", saved.getClerkId());
assertEquals("available", saved.getStatus());
}
@Test
void findWithdrawable_returnsEmptyWhenCounterLinesCreateDebt() {
LocalDateTime now = LocalDateTime.now();
List<EarningsLineEntity> lines = Arrays.asList(
line("neg", new BigDecimal("-60")),
line("pos", new BigDecimal("40")));
when(baseMapper.selectWithdrawableLines("clerk-001", now)).thenReturn(lines);
List<EarningsLineEntity> picked = earningsService.findWithdrawable("clerk-001", BigDecimal.valueOf(30), now);
assertEquals(0, picked.size(), "净额不足时不应允许提现");
}
@Test
void findWithdrawable_allowsWithdrawalAfterPositiveLinesCoverDebt() {
LocalDateTime now = LocalDateTime.now();
List<EarningsLineEntity> lines = Arrays.asList(
line("neg", new BigDecimal("-60")),
line("first", new BigDecimal("40")),
line("second", new BigDecimal("150")));
when(baseMapper.selectWithdrawableLines("clerk-001", now)).thenReturn(lines);
List<EarningsLineEntity> picked = earningsService.findWithdrawable("clerk-001", BigDecimal.valueOf(70), now);
assertEquals(3, picked.size());
assertEquals("second", picked.get(2).getId());
}
private EarningsLineEntity line(String id, BigDecimal amount) {
EarningsLineEntity entity = new EarningsLineEntity();
entity.setId(id);
entity.setAmount(amount);
entity.setStatus("available");
return entity;
}
}