重构收益与优惠券逻辑:统一使用 orderAmount,新增优惠券状态校验与过滤
- IPlayOrderInfoService/PlayOrderInfoServiceImpl/ClerkRevenueCalculator 将参数 finalAmount 更名为 orderAmount,避免语义混淆 - 预计收益计算兼容 null 与 0,防止 NPE 并明确边界 - 结算/回填:EarningsServiceImpl、EarningsBackfillServiceImpl 改为使用 orderMoney 兜底;0 金额允许,负数跳过 - 新增枚举:CouponOnlineState、CouponValidityPeriodType 用于券上下架与有效期判定 - IPlayCouponInfoService/Impl 增加 getCouponDetailRestrictionReason,支持已使用/过期/下架等状态校验 - WxCouponController 列表与下单查询增加状态/有效期/库存/白名单过滤逻辑 - OrderLifecycleServiceImpl 下单时校验优惠券状态,预计收益入参从 finalAmount 调整为 orderMoney - 完善单元测试:订单生命周期、优惠券过滤、收益生成与回填等覆盖
This commit is contained in:
@@ -58,6 +58,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -229,7 +230,7 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
|
||||
request.getPaymentInfo().getCouponIds(),
|
||||
request.getPlaceType().getCode(),
|
||||
YesNoFlag.YES.getCode(),
|
||||
request.getPaymentInfo().getFinalAmount());
|
||||
request.getPaymentInfo().getOrderMoney());
|
||||
|
||||
assertEquals(YesNoFlag.YES.getCode(), created.getFirstOrder());
|
||||
assertEquals(revenueVo.getRevenueAmount(), created.getEstimatedRevenue());
|
||||
@@ -904,6 +905,42 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
|
||||
assertMoneyEquals("25.00", result.getAmountBreakdown().getNetAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void placeOrder_rejectsCouponWhenRestrictionReasonPresent() {
|
||||
List<String> coupons = Collections.singletonList("coupon-reused");
|
||||
OrderCreationContext context = baseContext(
|
||||
PlaceType.SPECIFIED,
|
||||
RewardType.NOT_APPLICABLE,
|
||||
payment(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, coupons),
|
||||
null,
|
||||
"clerk-1");
|
||||
|
||||
stubDefaultPersistence();
|
||||
|
||||
PlayCouponDetailsEntity detail = new PlayCouponDetailsEntity();
|
||||
detail.setCouponId("coupon-master");
|
||||
detail.setUseState(CouponUseState.USED.getCode());
|
||||
detail.setObtainingTime(LocalDateTime.now().minusDays(1));
|
||||
when(playCouponDetailsService.getById("coupon-reused")).thenReturn(detail);
|
||||
|
||||
PlayCouponInfoEntity info = new PlayCouponInfoEntity();
|
||||
info.setCouponOnLineState("1");
|
||||
info.setValidityPeriodType("0");
|
||||
when(playCouponInfoService.selectPlayCouponInfoById("coupon-master")).thenReturn(info);
|
||||
when(playCouponInfoService.getCouponDetailRestrictionReason(eq(info), eq(detail.getUseState()), any(LocalDateTime.class)))
|
||||
.thenReturn(Optional.of("优惠券已使用"));
|
||||
|
||||
CustomException exception = assertThrows(CustomException.class, () -> lifecycleService.placeOrder(command(
|
||||
context,
|
||||
pricing(BigDecimal.valueOf(30), 1, coupons, "commodity-reused", PlaceType.SPECIFIED),
|
||||
false,
|
||||
null,
|
||||
null)));
|
||||
|
||||
assertEquals("优惠券不可用 - 优惠券已使用", exception.getMessage());
|
||||
verify(playCouponInfoService).getCouponDetailRestrictionReason(eq(info), eq(detail.getUseState()), any(LocalDateTime.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void placeOrder_withDeductionFetchesCustomerBalance() {
|
||||
OrderCreationContext context = baseContext(
|
||||
@@ -1157,6 +1194,8 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
|
||||
lenient().when(orderInfoMapper.insert(any())).thenReturn(1);
|
||||
lenient().doNothing().when(customUserInfoService).saveOrderInfo(any());
|
||||
lenient().doNothing().when(playCouponDetailsService).updateCouponUseStateByIds(anyList(), anyString());
|
||||
lenient().when(playCouponInfoService.getCouponDetailRestrictionReason(any(), any(), any()))
|
||||
.thenReturn(Optional.empty());
|
||||
ClerkEstimatedRevenueVo revenueVo = new ClerkEstimatedRevenueVo();
|
||||
revenueVo.setRevenueAmount(BigDecimal.ZERO);
|
||||
revenueVo.setRevenueRatio(0);
|
||||
@@ -1236,6 +1275,8 @@ private PlayOrderLogInfoMapper orderLogInfoMapper;
|
||||
when(playCouponInfoService.selectPlayCouponInfoById(masterId)).thenReturn(info);
|
||||
when(playCouponInfoService.getCouponReasonForUnavailableUse(
|
||||
eq(info), anyString(), anyString(), anyInt(), any())).thenReturn(reason);
|
||||
when(playCouponInfoService.getCouponDetailRestrictionReason(eq(info), any(), any()))
|
||||
.thenReturn(Optional.empty());
|
||||
}
|
||||
|
||||
private PlayCustomUserInfoEntity customer(String id, BigDecimal balance) {
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
package com.starry.admin.modules.shop.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.starry.admin.common.conf.ThreadLocalRequestDetail;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
|
||||
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||
import com.starry.admin.modules.shop.service.impl.PlayCouponInfoServiceImpl;
|
||||
import com.starry.admin.modules.weichat.controller.WxCouponController;
|
||||
import com.starry.admin.modules.weichat.entity.WxCouponOrderQueryVo;
|
||||
import com.starry.admin.modules.weichat.entity.WxCouponOrderReturnVo;
|
||||
import com.starry.admin.modules.weichat.entity.WxCouponReceiveReturnVo;
|
||||
import com.starry.common.result.R;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -97,4 +108,105 @@ public class CouponWhitelistTest {
|
||||
assertEquals(1, list.size(), "非白名单券应被过滤不可见");
|
||||
assertEquals("c1", list.get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("列表过滤-状态:下架或过期优惠券不可见")
|
||||
void testQueryAllFiltersInvalidStatus() {
|
||||
PlayCustomUserInfoEntity current = new PlayCustomUserInfoEntity();
|
||||
current.setId("uid-2");
|
||||
ThreadLocalRequestDetail.setRequestDetail(current);
|
||||
|
||||
PlayCouponInfoEntity offline = new PlayCouponInfoEntity();
|
||||
offline.setId("offline");
|
||||
offline.setCouponOnLineState(CouponOnlineState.OFFLINE.getCode());
|
||||
|
||||
PlayCouponInfoEntity expired = new PlayCouponInfoEntity();
|
||||
expired.setId("expired");
|
||||
expired.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||
expired.setValidityPeriodType(CouponValidityPeriodType.FIXED_RANGE.getCode());
|
||||
expired.setProductiveTime(LocalDateTime.now().minusDays(5));
|
||||
expired.setExpirationTime(LocalDateTime.now().minusDays(1));
|
||||
expired.setRemainingQuantity(5);
|
||||
|
||||
PlayCouponInfoEntity active = new PlayCouponInfoEntity();
|
||||
active.setId("active");
|
||||
active.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||
active.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||
active.setRemainingQuantity(10);
|
||||
|
||||
when(couponDetailsService.selectByCustomId("uid-2")).thenReturn(Collections.emptyList());
|
||||
when(couponInfoService.queryAll()).thenReturn(Arrays.asList(offline, expired, active));
|
||||
|
||||
R resp = wxCouponController.queryAll();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<WxCouponReceiveReturnVo> list = (List<WxCouponReceiveReturnVo>) resp.getData();
|
||||
|
||||
assertNotNull(list);
|
||||
assertEquals(1, list.size(), "仅应展示在线有效的优惠券");
|
||||
assertEquals("active", list.get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("下单查询:存在限制原因的优惠券被过滤")
|
||||
void testQueryByOrderFiltersRestrictedCoupons() {
|
||||
PlayCustomUserInfoEntity current = new PlayCustomUserInfoEntity();
|
||||
current.setId("uid-3");
|
||||
ThreadLocalRequestDetail.setRequestDetail(current);
|
||||
|
||||
PlayCouponDetailsReturnVo restricted = new PlayCouponDetailsReturnVo();
|
||||
restricted.setId("detail-1");
|
||||
restricted.setCouponId("coupon-restrict");
|
||||
restricted.setUseState(CouponUseState.USED.getCode());
|
||||
restricted.setObtainingTime(LocalDateTime.now().minusDays(3));
|
||||
|
||||
PlayCouponDetailsReturnVo usable = new PlayCouponDetailsReturnVo();
|
||||
usable.setId("detail-2");
|
||||
usable.setCouponId("coupon-usable");
|
||||
usable.setUseState(CouponUseState.UNUSED.getCode());
|
||||
usable.setObtainingTime(LocalDateTime.now().minusDays(1));
|
||||
|
||||
when(couponDetailsService.selectByCustomId("uid-3"))
|
||||
.thenReturn(Arrays.asList(restricted, usable));
|
||||
|
||||
PlayCouponInfoEntity restrictedInfo = new PlayCouponInfoEntity();
|
||||
restrictedInfo.setId("coupon-restrict");
|
||||
restrictedInfo.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||
restrictedInfo.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||
|
||||
PlayCouponInfoEntity usableInfo = new PlayCouponInfoEntity();
|
||||
usableInfo.setId("coupon-usable");
|
||||
usableInfo.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||
usableInfo.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||
|
||||
when(couponInfoService.selectPlayCouponInfoById("coupon-restrict")).thenReturn(restrictedInfo);
|
||||
when(couponInfoService.selectPlayCouponInfoById("coupon-usable")).thenReturn(usableInfo);
|
||||
when(couponInfoService.getCouponDetailRestrictionReason(eq(restrictedInfo), eq(restricted.getUseState()), any(LocalDateTime.class)))
|
||||
.thenReturn(Optional.of("优惠券已使用"));
|
||||
when(couponInfoService.getCouponDetailRestrictionReason(eq(usableInfo), eq(usable.getUseState()), any(LocalDateTime.class)))
|
||||
.thenReturn(Optional.empty());
|
||||
when(couponInfoService.getCouponReasonForUnavailableUse(eq(usableInfo), anyString(), anyString(), anyInt(), any()))
|
||||
.thenReturn("");
|
||||
|
||||
PlayCommodityInfoVo commodityInfo = new PlayCommodityInfoVo();
|
||||
commodityInfo.setCommodityId("cmd-1");
|
||||
commodityInfo.setCommodityPrice(BigDecimal.valueOf(50));
|
||||
when(commodityInfoService.queryCommodityInfo("cmd-1", "level-1")).thenReturn(commodityInfo);
|
||||
|
||||
WxCouponOrderQueryVo vo = new WxCouponOrderQueryVo();
|
||||
vo.setCommodityId("cmd-1");
|
||||
vo.setLevelId("level-1");
|
||||
vo.setClerkId("");
|
||||
vo.setPlaceType("0");
|
||||
vo.setCommodityQuantity(1);
|
||||
|
||||
R resp = wxCouponController.queryByOrder(vo);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<WxCouponOrderReturnVo> list = (List<WxCouponOrderReturnVo>) resp.getData();
|
||||
|
||||
assertNotNull(list);
|
||||
assertEquals(1, list.size(), "受限优惠券应被过滤");
|
||||
assertEquals("detail-2", list.get(0).getId());
|
||||
assertEquals("1", list.get(0).getAvailable());
|
||||
verify(couponInfoService).getCouponDetailRestrictionReason(eq(restrictedInfo), eq(restricted.getUseState()), any(LocalDateTime.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.starry.admin.modules.withdraw.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class EarningsBackfillServiceImplTest {
|
||||
|
||||
private final EarningsBackfillServiceImpl service = new EarningsBackfillServiceImpl();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private BigDecimal invokeResolveAmount(PlayOrderInfoEntity order, List<String> warnings) throws Exception {
|
||||
Method method = EarningsBackfillServiceImpl.class.getDeclaredMethod("resolveAmount", PlayOrderInfoEntity.class,
|
||||
List.class);
|
||||
method.setAccessible(true);
|
||||
return (BigDecimal) method.invoke(service, order, warnings);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveAmount_acceptsZeroAmountWithoutWarnings() throws Exception {
|
||||
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||
order.setId("order-zero");
|
||||
order.setAcceptBy("clerk-001");
|
||||
order.setOrderEndTime(LocalDateTime.now());
|
||||
order.setOrderMoney(BigDecimal.ZERO);
|
||||
|
||||
List<String> warnings = new ArrayList<>();
|
||||
BigDecimal amount = invokeResolveAmount(order, warnings);
|
||||
|
||||
assertEquals(BigDecimal.ZERO, amount);
|
||||
assertTrue(warnings.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveAmount_rejectsNegativeAmount() throws Exception {
|
||||
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||
order.setId("order-negative");
|
||||
order.setAcceptBy("clerk-001");
|
||||
order.setOrderEndTime(LocalDateTime.now());
|
||||
order.setEstimatedRevenue(BigDecimal.valueOf(-5));
|
||||
|
||||
List<String> warnings = new ArrayList<>();
|
||||
BigDecimal amount = invokeResolveAmount(order, warnings);
|
||||
|
||||
assertNull(amount);
|
||||
assertEquals(1, warnings.size());
|
||||
assertTrue(warnings.get(0).contains("order-negative"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.starry.admin.modules.withdraw.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||
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 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.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class EarningsServiceImplTest {
|
||||
|
||||
@InjectMocks
|
||||
private EarningsServiceImpl earningsService;
|
||||
|
||||
@Mock
|
||||
private EarningsLineMapper baseMapper;
|
||||
|
||||
@Mock
|
||||
private IFreezePolicyService freezePolicyService;
|
||||
|
||||
private PlayOrderInfoEntity baselineOrder() {
|
||||
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||
order.setId("order-001");
|
||||
order.setTenantId("tenant-001");
|
||||
order.setAcceptBy("clerk-001");
|
||||
order.setOrderEndTime(LocalDateTime.now());
|
||||
order.setOrderSettlementState("0");
|
||||
return order;
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromOrder_usesOrderAmountWhenEstimatedMissing_evenIfZero() {
|
||||
PlayOrderInfoEntity order = baselineOrder();
|
||||
order.setEstimatedRevenue(null);
|
||||
order.setOrderMoney(BigDecimal.ZERO);
|
||||
|
||||
when(freezePolicyService.resolveFreezeHours("tenant-001", "clerk-001")).thenReturn(0);
|
||||
when(baseMapper.insert(any())).thenReturn(1);
|
||||
|
||||
earningsService.createFromOrder(order);
|
||||
|
||||
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
|
||||
verify(baseMapper).insert(captor.capture());
|
||||
assertEquals(BigDecimal.ZERO, captor.getValue().getAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromOrder_skipsWhenAmountNegative() {
|
||||
PlayOrderInfoEntity order = baselineOrder();
|
||||
order.setEstimatedRevenue(BigDecimal.valueOf(-10));
|
||||
order.setOrderMoney(BigDecimal.valueOf(20));
|
||||
|
||||
earningsService.createFromOrder(order);
|
||||
|
||||
verify(baseMapper, never()).insert(any());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user