Compare commits
7 Commits
cc76710858
...
5331fd75a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5331fd75a2 | ||
|
|
5c0de2201c | ||
|
|
29f168dd67 | ||
|
|
48348609a8 | ||
|
|
25554bac84 | ||
|
|
cec5e965f6 | ||
|
|
4cd2950051 |
@@ -54,6 +54,7 @@ import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -69,9 +70,7 @@ import org.springframework.stereotype.Service;
|
||||
* @since 2024-03-30
|
||||
*/
|
||||
@Service
|
||||
public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoMapper, PlayClerkUserInfoEntity>
|
||||
implements
|
||||
IPlayClerkUserInfoService {
|
||||
public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoMapper, PlayClerkUserInfoEntity> implements IPlayClerkUserInfoService {
|
||||
|
||||
private static final String OFFBOARD_MESSAGE = "你已离职,需要复职请联系店铺管理员";
|
||||
private static final String DELISTED_MESSAGE = "你已被下架,没有权限访问";
|
||||
@@ -131,8 +130,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
MPJLambdaWrapper<PlayClerkUserInfoEntity> lambdaWrapper = new MPJLambdaWrapper<>();
|
||||
lambdaWrapper.selectAll(PlayClerkLevelInfoEntity.class);
|
||||
lambdaWrapper.selectAs(PlayClerkUserInfoEntity::getLevelId, "levelId");
|
||||
lambdaWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId,
|
||||
PlayClerkUserInfoEntity::getLevelId);
|
||||
lambdaWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId, PlayClerkUserInfoEntity::getLevelId);
|
||||
lambdaWrapper.eq(PlayClerkUserInfoEntity::getId, clerkId);
|
||||
PlayClerkLevelInfoEntity levelInfo = this.baseMapper.selectJoinOne(PlayClerkLevelInfoEntity.class, lambdaWrapper);
|
||||
if (levelInfo != null) {
|
||||
@@ -157,8 +155,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
/**
|
||||
* 查询店员
|
||||
*
|
||||
* @param id
|
||||
* 店员主键
|
||||
* @param id 店员主键
|
||||
* @return 店员
|
||||
*/
|
||||
@Override
|
||||
@@ -173,13 +170,9 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
@Override
|
||||
public PlayClerkUserLoginResponseVo getVo(PlayClerkUserInfoEntity userInfo) {
|
||||
PlayClerkUserLoginResponseVo result = ConvertUtil.entityToVo(userInfo, PlayClerkUserLoginResponseVo.class);
|
||||
List<PlayClerkDataReviewInfoEntity> pendingReviews = playClerkDataReviewInfoService
|
||||
.queryByClerkId(userInfo.getId(), "0");
|
||||
List<PlayClerkDataReviewInfoEntity> pendingReviews = playClerkDataReviewInfoService.queryByClerkId(userInfo.getId(), "0");
|
||||
if (pendingReviews != null && !pendingReviews.isEmpty()) {
|
||||
Set<String> pendingTypes = pendingReviews.stream()
|
||||
.map(PlayClerkDataReviewInfoEntity::getDataType)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
Set<String> pendingTypes = pendingReviews.stream().map(PlayClerkDataReviewInfoEntity::getDataType).filter(StrUtil::isNotBlank).collect(Collectors.toSet());
|
||||
if (pendingTypes.contains("0")) {
|
||||
result.setNicknameAllowEdit(false);
|
||||
}
|
||||
@@ -217,12 +210,10 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
}
|
||||
|
||||
// 查询店员服务项目
|
||||
List<PlayClerkCommodityEntity> clerkCommodityEntities = playClerkCommodityService
|
||||
.selectCommodityTypeByUser(userInfo.getId(), "");
|
||||
List<PlayClerkCommodityEntity> clerkCommodityEntities = playClerkCommodityService.selectCommodityTypeByUser(userInfo.getId(), "");
|
||||
List<PlayClerkCommodityQueryVo> playClerkCommodityQueryVos = new ArrayList<>();
|
||||
for (PlayClerkCommodityEntity clerkCommodityEntity : clerkCommodityEntities) {
|
||||
playClerkCommodityQueryVos.add(new PlayClerkCommodityQueryVo(clerkCommodityEntity.getCommodityName(),
|
||||
clerkCommodityEntity.getEnablingState()));
|
||||
playClerkCommodityQueryVos.add(new PlayClerkCommodityQueryVo(clerkCommodityEntity.getCommodityName(), clerkCommodityEntity.getEnablingState()));
|
||||
}
|
||||
result.setCommodity(playClerkCommodityQueryVos);
|
||||
result.setArea(userInfo.getProvince() + "-" + userInfo.getCity());
|
||||
@@ -265,10 +256,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
if (StrUtil.isBlank(clerkId)) {
|
||||
return;
|
||||
}
|
||||
LambdaUpdateWrapper<PlayClerkUserInfoEntity> wrapper = Wrappers.lambdaUpdate(PlayClerkUserInfoEntity.class)
|
||||
.eq(PlayClerkUserInfoEntity::getId, clerkId)
|
||||
.set(PlayClerkUserInfoEntity::getToken, "empty")
|
||||
.set(PlayClerkUserInfoEntity::getOnlineState, "0");
|
||||
LambdaUpdateWrapper<PlayClerkUserInfoEntity> wrapper = Wrappers.lambdaUpdate(PlayClerkUserInfoEntity.class).eq(PlayClerkUserInfoEntity::getId, clerkId).set(PlayClerkUserInfoEntity::getToken, "empty").set(PlayClerkUserInfoEntity::getOnlineState, "0");
|
||||
this.baseMapper.update(null, wrapper);
|
||||
}
|
||||
|
||||
@@ -286,21 +274,17 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAccountBalanceById(String userId, BigDecimal balanceBeforeOperation,
|
||||
BigDecimal balanceAfterOperation, String operationType, String operationAction, BigDecimal balanceMoney,
|
||||
String orderId) {
|
||||
public void updateAccountBalanceById(String userId, BigDecimal balanceBeforeOperation, BigDecimal balanceAfterOperation, String operationType, String operationAction, BigDecimal balanceMoney, String orderId) {
|
||||
// 修改用户余额
|
||||
this.baseMapper.updateById(new PlayClerkUserInfoEntity(userId, balanceAfterOperation));
|
||||
// 记录余额变更记录
|
||||
playBalanceDetailsInfoService.insertBalanceDetailsInfo("0", userId, balanceBeforeOperation,
|
||||
balanceAfterOperation, operationType, operationAction, balanceMoney, BigDecimal.ZERO, orderId);
|
||||
playBalanceDetailsInfoService.insertBalanceDetailsInfo("0", userId, balanceBeforeOperation, balanceAfterOperation, operationType, operationAction, balanceMoney, BigDecimal.ZERO, orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询店员列表
|
||||
*
|
||||
* @param vo
|
||||
* 店员查询对象
|
||||
* @param vo 店员查询对象
|
||||
* @return 店员
|
||||
*/
|
||||
@Override
|
||||
@@ -311,12 +295,10 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
// 查询不隐藏的
|
||||
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getDisplayState, "1");
|
||||
// 查询主表全部字段
|
||||
lambdaQueryWrapper.selectAll(PlayClerkUserInfoEntity.class).selectAs(PlayClerkUserInfoEntity::getCity,
|
||||
"address");
|
||||
lambdaQueryWrapper.selectAll(PlayClerkUserInfoEntity.class).selectAs(PlayClerkUserInfoEntity::getCity, "address");
|
||||
// 等级表
|
||||
lambdaQueryWrapper.selectAs(PlayClerkLevelInfoEntity::getName, "levelName");
|
||||
lambdaQueryWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId,
|
||||
PlayClerkUserInfoEntity::getLevelId);
|
||||
lambdaQueryWrapper.leftJoin(PlayClerkLevelInfoEntity.class, PlayClerkLevelInfoEntity::getId, PlayClerkUserInfoEntity::getLevelId);
|
||||
|
||||
// 服务项目表
|
||||
if (StrUtil.isNotBlank(vo.getNickname())) {
|
||||
@@ -345,11 +327,28 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
}
|
||||
|
||||
// 排序:非空的等级排序号优先,值越小越靠前;同一排序号在线状态优先
|
||||
lambdaQueryWrapper.orderByAsc(true, "CASE WHEN t1.order_number IS NULL THEN 1 ELSE 0 END")
|
||||
.orderByAsc(PlayClerkLevelInfoEntity::getOrderNumber)
|
||||
.orderByDesc(PlayClerkUserInfoEntity::getOnlineState);
|
||||
lambdaQueryWrapper
|
||||
.orderByDesc(PlayClerkUserInfoEntity::getPinToTopState)
|
||||
.orderByDesc(PlayClerkUserInfoEntity::getOnlineState)
|
||||
.orderByAsc(true, "CASE WHEN t1.order_number IS NULL THEN 1 ELSE 0 END")
|
||||
.orderByAsc(PlayClerkLevelInfoEntity::getOrderNumber)
|
||||
.orderByAsc(PlayClerkUserInfoEntity::getCreatedTime)
|
||||
.orderByAsc(PlayClerkUserInfoEntity::getId);
|
||||
|
||||
return this.baseMapper.selectJoinPage(page, PlayClerkUserInfoResultVo.class, lambdaQueryWrapper);
|
||||
IPage<PlayClerkUserInfoResultVo> rawPage = this.baseMapper.selectJoinPage(page, PlayClerkUserInfoResultVo.class, lambdaQueryWrapper);
|
||||
if (rawPage != null && rawPage.getRecords() != null) {
|
||||
List<PlayClerkUserInfoResultVo> deduped = new ArrayList<>();
|
||||
Set<String> seen = new HashSet<>();
|
||||
for (PlayClerkUserInfoResultVo record : rawPage.getRecords()) {
|
||||
String id = record.getId();
|
||||
if (id == null || !seen.add(id)) {
|
||||
continue;
|
||||
}
|
||||
deduped.add(record);
|
||||
}
|
||||
rawPage.setRecords(deduped);
|
||||
}
|
||||
return rawPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -364,8 +363,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
public IPage<PlayClerkUnsettledWagesInfoReturnVo> listUnsettledWagesByPage(PlayClerkUnsettledWagesInfoQueryVo vo) {
|
||||
MPJLambdaWrapper<PlayClerkUserInfoEntity> lambdaQueryWrapper = new MPJLambdaWrapper<>();
|
||||
// 查询所有店员
|
||||
lambdaQueryWrapper.selectAs(PlayClerkUserInfoEntity::getNickname, "clerkNickname")
|
||||
.selectAs(PlayClerkUserInfoEntity::getId, "clerkId");
|
||||
lambdaQueryWrapper.selectAs(PlayClerkUserInfoEntity::getNickname, "clerkNickname").selectAs(PlayClerkUserInfoEntity::getId, "clerkId");
|
||||
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getClerkState, ClerkRoleStatus.CLERK.getCode());
|
||||
// 加入组员的筛选
|
||||
List<String> clerkIdList = playClerkGroupInfoService.getValidClerkIdList(SecurityUtils.getLoginUser(), null);
|
||||
@@ -377,14 +375,11 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getListingState, vo.getListingState());
|
||||
}
|
||||
// 查询店员订单信息
|
||||
lambdaQueryWrapper.selectCollection(PlayOrderInfoEntity.class,
|
||||
PlayClerkUnsettledWagesInfoReturnVo::getOrderInfoEntities);
|
||||
lambdaQueryWrapper.leftJoin(PlayOrderInfoEntity.class, PlayOrderInfoEntity::getAcceptBy,
|
||||
PlayClerkUserInfoEntity::getId);
|
||||
lambdaQueryWrapper.selectCollection(PlayOrderInfoEntity.class, PlayClerkUnsettledWagesInfoReturnVo::getOrderInfoEntities);
|
||||
lambdaQueryWrapper.leftJoin(PlayOrderInfoEntity.class, PlayOrderInfoEntity::getAcceptBy, PlayClerkUserInfoEntity::getId);
|
||||
lambdaQueryWrapper.eq(PlayOrderInfoEntity::getOrderSettlementState, "0");
|
||||
|
||||
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()),
|
||||
PlayClerkUnsettledWagesInfoReturnVo.class, lambdaQueryWrapper);
|
||||
return this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkUnsettledWagesInfoReturnVo.class, lambdaQueryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -483,12 +478,9 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
lambdaQueryWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);
|
||||
|
||||
// 排序:置顶状态优先,在线用户其次,最后按创建时间倒序
|
||||
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getPinToTopState)
|
||||
.orderByDesc(PlayClerkUserInfoEntity::getOnlineState)
|
||||
.orderByDesc(PlayClerkUserInfoEntity::getCreatedTime);
|
||||
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getPinToTopState).orderByDesc(PlayClerkUserInfoEntity::getOnlineState).orderByDesc(PlayClerkUserInfoEntity::getCreatedTime);
|
||||
|
||||
IPage<PlayClerkUserReturnVo> page = this.baseMapper.selectJoinPage(
|
||||
new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkUserReturnVo.class, lambdaQueryWrapper);
|
||||
IPage<PlayClerkUserReturnVo> page = this.baseMapper.selectJoinPage(new Page<>(vo.getPageNum(), vo.getPageSize()), PlayClerkUserReturnVo.class, lambdaQueryWrapper);
|
||||
|
||||
for (PlayClerkUserReturnVo record : page.getRecords()) {
|
||||
BigDecimal orderTotalAmount = new BigDecimal("0");
|
||||
@@ -519,10 +511,8 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
if (StrUtil.isNotBlank(customUserId)) {
|
||||
LambdaQueryWrapper<PlayCustomFollowInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
lambdaQueryWrapper.eq(PlayCustomFollowInfoEntity::getCustomId, customUserId);
|
||||
List<PlayCustomFollowInfoEntity> customFollowInfoEntities = customFollowInfoService
|
||||
.list(lambdaQueryWrapper);
|
||||
customFollows = customFollowInfoEntities.stream().collect(Collectors
|
||||
.toMap(PlayCustomFollowInfoEntity::getClerkId, PlayCustomFollowInfoEntity::getFollowState));
|
||||
List<PlayCustomFollowInfoEntity> customFollowInfoEntities = customFollowInfoService.list(lambdaQueryWrapper);
|
||||
customFollows = customFollowInfoEntities.stream().collect(Collectors.toMap(PlayCustomFollowInfoEntity::getClerkId, PlayCustomFollowInfoEntity::getFollowState));
|
||||
}
|
||||
for (PlayClerkUserInfoResultVo record : voPage.getRecords()) {
|
||||
record.setFollowState(customFollows.containsKey(record.getId()) ? "1" : "0");
|
||||
@@ -537,8 +527,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
/**
|
||||
* 新增店员
|
||||
*
|
||||
* @param playClerkUserInfo
|
||||
* 店员
|
||||
* @param playClerkUserInfo 店员
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@@ -552,16 +541,12 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
/**
|
||||
* 修改店员
|
||||
*
|
||||
* @param playClerkUserInfo
|
||||
* 店员
|
||||
* @param playClerkUserInfo 店员
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public boolean update(PlayClerkUserInfoEntity playClerkUserInfo) {
|
||||
boolean inspectStatus = StringUtils.isNotBlank(playClerkUserInfo.getId())
|
||||
&& (StrUtil.isNotBlank(playClerkUserInfo.getOnboardingState())
|
||||
|| StrUtil.isNotBlank(playClerkUserInfo.getListingState())
|
||||
|| StrUtil.isNotBlank(playClerkUserInfo.getClerkState()));
|
||||
boolean inspectStatus = StringUtils.isNotBlank(playClerkUserInfo.getId()) && (StrUtil.isNotBlank(playClerkUserInfo.getOnboardingState()) || StrUtil.isNotBlank(playClerkUserInfo.getListingState()) || StrUtil.isNotBlank(playClerkUserInfo.getClerkState()));
|
||||
PlayClerkUserInfoEntity beforeUpdate = null;
|
||||
if (inspectStatus) {
|
||||
beforeUpdate = this.baseMapper.selectById(playClerkUserInfo.getId());
|
||||
@@ -576,8 +561,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
/**
|
||||
* 批量删除店员
|
||||
*
|
||||
* @param ids
|
||||
* 需要删除的店员主键
|
||||
* @param ids 需要删除的店员主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@@ -588,8 +572,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
/**
|
||||
* 删除店员信息
|
||||
*
|
||||
* @param id
|
||||
* 店员主键
|
||||
* @param id 店员主键
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
@@ -603,9 +586,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
LambdaQueryWrapper<PlayClerkUserInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
lambdaQueryWrapper.eq(PlayClerkUserInfoEntity::getDeleted, 0);
|
||||
lambdaQueryWrapper.in(PlayClerkUserInfoEntity::getId, clerkIdList);
|
||||
lambdaQueryWrapper.select(PlayClerkUserInfoEntity::getId, PlayClerkUserInfoEntity::getNickname,
|
||||
PlayClerkUserInfoEntity::getAvatar, PlayClerkUserInfoEntity::getTypeId,
|
||||
PlayClerkUserInfoEntity::getGroupId, PlayClerkUserInfoEntity::getPhone);
|
||||
lambdaQueryWrapper.select(PlayClerkUserInfoEntity::getId, PlayClerkUserInfoEntity::getNickname, PlayClerkUserInfoEntity::getAvatar, PlayClerkUserInfoEntity::getTypeId, PlayClerkUserInfoEntity::getGroupId, PlayClerkUserInfoEntity::getPhone);
|
||||
lambdaQueryWrapper.orderByDesc(PlayClerkUserInfoEntity::getId);
|
||||
return this.baseMapper.selectList(lambdaQueryWrapper);
|
||||
}
|
||||
@@ -621,8 +602,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
LoginUser loginUserInfo = loginService.getLoginUserInfo(entity.getSysUserId());
|
||||
Map<String, Object> tokenMap = jwtToken.createToken(loginUserInfo);
|
||||
data.fluentPut("token", tokenMap.get("token"));
|
||||
PlayPersonnelAdminInfoEntity adminInfoEntity = playPersonnelAdminInfoService
|
||||
.selectByUserId(entity.getSysUserId());
|
||||
PlayPersonnelAdminInfoEntity adminInfoEntity = playPersonnelAdminInfoService.selectByUserId(entity.getSysUserId());
|
||||
if (Objects.nonNull(adminInfoEntity)) {
|
||||
data.fluentPut("role", "operator");
|
||||
return data;
|
||||
@@ -632,8 +612,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
data.fluentPut("role", "leader");
|
||||
return data;
|
||||
}
|
||||
PlayPersonnelWaiterInfoEntity waiterInfoEntity = playClerkWaiterInfoService
|
||||
.selectByUserId(entity.getSysUserId());
|
||||
PlayPersonnelWaiterInfoEntity waiterInfoEntity = playClerkWaiterInfoService.selectByUserId(entity.getSysUserId());
|
||||
if (Objects.nonNull(waiterInfoEntity)) {
|
||||
data.fluentPut("role", "waiter");
|
||||
return data;
|
||||
@@ -645,12 +624,7 @@ public class PlayClerkUserInfoServiceImpl extends ServiceImpl<PlayClerkUserInfoM
|
||||
if (beforeUpdate == null) {
|
||||
return;
|
||||
}
|
||||
if (OnboardingStatus.transitionedToOffboarded(updatedPayload.getOnboardingState(),
|
||||
beforeUpdate.getOnboardingState())
|
||||
|| ListingStatus.transitionedToDelisted(updatedPayload.getListingState(),
|
||||
beforeUpdate.getListingState())
|
||||
|| ClerkRoleStatus.transitionedToNonClerk(updatedPayload.getClerkState(),
|
||||
beforeUpdate.getClerkState())) {
|
||||
if (OnboardingStatus.transitionedToOffboarded(updatedPayload.getOnboardingState(), beforeUpdate.getOnboardingState()) || ListingStatus.transitionedToDelisted(updatedPayload.getListingState(), beforeUpdate.getListingState()) || ClerkRoleStatus.transitionedToNonClerk(updatedPayload.getClerkState(), beforeUpdate.getClerkState())) {
|
||||
invalidateClerkSession(beforeUpdate.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||
import com.starry.admin.modules.shop.module.vo.PlayCommodityInfoVo;
|
||||
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
||||
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||
import com.starry.admin.utils.SecurityUtils;
|
||||
import com.starry.common.annotation.Log;
|
||||
import com.starry.common.context.CustomSecurityContextHolder;
|
||||
@@ -28,6 +29,7 @@ import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -59,6 +61,9 @@ public class PlayOrderInfoController {
|
||||
@Resource
|
||||
private IPlayClerkUserInfoService clerkUserInfoService;
|
||||
|
||||
@Resource
|
||||
private IEarningsService earningsService;
|
||||
|
||||
/**
|
||||
* 分页查询订单列表
|
||||
*/
|
||||
@@ -115,11 +120,8 @@ public class PlayOrderInfoController {
|
||||
context.setRefundToCustomer(vo.isRefundToCustomer());
|
||||
context.setRefundAmount(vo.getRefundAmount());
|
||||
context.setRefundReason(vo.getRefundReason());
|
||||
OrderRevocationContext.EarningsAdjustStrategy strategy = vo.getEarningsStrategy() != null
|
||||
? vo.getEarningsStrategy()
|
||||
: OrderRevocationContext.EarningsAdjustStrategy.NONE;
|
||||
context.setEarningsStrategy(strategy);
|
||||
context.setCounterClerkId(vo.getCounterClerkId());
|
||||
context.setDeductClerkEarnings(vo.isDeductClerkEarnings());
|
||||
context.setEarningsAdjustAmount(vo.getDeductAmount());
|
||||
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setOperatorId(SecurityUtils.getUserId());
|
||||
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
|
||||
@@ -127,6 +129,29 @@ public class PlayOrderInfoController {
|
||||
return R.ok("撤销成功");
|
||||
}
|
||||
|
||||
@ApiOperation(value = "撤销限额", notes = "查询指定订单可退金额与可扣回收益")
|
||||
@GetMapping("/{id}/revocationLimits")
|
||||
public R getRevocationLimits(@PathVariable("id") String id) {
|
||||
PlayOrderInfoEntity order = orderInfoService.selectOrderInfoById(id);
|
||||
BigDecimal maxRefundAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
|
||||
BigDecimal maxDeductAmount = BigDecimal.ZERO;
|
||||
if (order.getAcceptBy() != null) {
|
||||
maxDeductAmount = Optional.ofNullable(earningsService.getRemainingEarningsForOrder(order.getId(), order.getAcceptBy()))
|
||||
.orElse(BigDecimal.ZERO);
|
||||
}
|
||||
if (maxDeductAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
maxDeductAmount = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
PlayOrderRevocationLimitsVo limitsVo = new PlayOrderRevocationLimitsVo();
|
||||
limitsVo.setOrderId(order.getId());
|
||||
limitsVo.setMaxRefundAmount(maxRefundAmount);
|
||||
limitsVo.setMaxDeductAmount(maxDeductAmount);
|
||||
limitsVo.setDefaultDeductAmount(maxDeductAmount);
|
||||
limitsVo.setDeductible(order.getAcceptBy() != null);
|
||||
return R.ok(limitsVo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理后台强制取消进行中订单
|
||||
*/
|
||||
|
||||
@@ -26,30 +26,29 @@ public class OrderRevocationEarningsListener {
|
||||
return;
|
||||
}
|
||||
OrderRevocationContext context = event.getContext();
|
||||
switch (context.getEarningsStrategy()) {
|
||||
case NONE:
|
||||
return;
|
||||
case REVERSE_CLERK:
|
||||
earningsService.reverseByOrder(event.getOrderSnapshot().getId(), context.getOperatorId());
|
||||
return;
|
||||
case COUNTER_TO_PEIPEI:
|
||||
createCounterLine(event);
|
||||
return;
|
||||
default:
|
||||
throw new CustomException("未知的收益处理策略");
|
||||
if (!context.isDeductClerkEarnings()) {
|
||||
return;
|
||||
}
|
||||
|
||||
createCounterLine(event);
|
||||
}
|
||||
|
||||
private void createCounterLine(OrderRevocationEvent event) {
|
||||
OrderRevocationContext context = event.getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
PlayOrderInfoEntity order = event.getOrderSnapshot();
|
||||
String targetClerkId = context.getCounterClerkId();
|
||||
if (targetClerkId == null) {
|
||||
String targetClerkId = order.getAcceptBy();
|
||||
if (targetClerkId == null || targetClerkId.trim().isEmpty()) {
|
||||
throw new CustomException("需要指定收益冲销目标账号");
|
||||
}
|
||||
BigDecimal amount = context.getRefundAmount();
|
||||
BigDecimal amount = context.getEarningsAdjustAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
amount = Optional.ofNullable(order.getEstimatedRevenue()).orElse(BigDecimal.ZERO);
|
||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
earningsService.createCounterLine(order.getId(), order.getTenantId(), targetClerkId, amount, context.getOperatorId());
|
||||
}
|
||||
|
||||
@@ -26,31 +26,15 @@ public class OrderRevocationContext {
|
||||
|
||||
private boolean refundToCustomer;
|
||||
|
||||
private EarningsAdjustStrategy earningsStrategy = EarningsAdjustStrategy.NONE;
|
||||
|
||||
@Nullable
|
||||
private String counterClerkId;
|
||||
private boolean deductClerkEarnings;
|
||||
|
||||
private OrderTriggerSource triggerSource = OrderTriggerSource.UNKNOWN;
|
||||
|
||||
@Nullable
|
||||
private BigDecimal earningsAdjustAmount;
|
||||
|
||||
public OrderRevocationContext withTriggerSource(OrderTriggerSource triggerSource) {
|
||||
this.triggerSource = triggerSource;
|
||||
return this;
|
||||
}
|
||||
|
||||
public enum EarningsAdjustStrategy {
|
||||
NONE("NO_ADJUST"),
|
||||
REVERSE_CLERK("REV_CLERK"),
|
||||
COUNTER_TO_PEIPEI("CTR_PEIPEI");
|
||||
|
||||
private final String logCode;
|
||||
|
||||
EarningsAdjustStrategy(String logCode) {
|
||||
this.logCode = logCode;
|
||||
}
|
||||
|
||||
public String getLogCode() {
|
||||
return logCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.starry.admin.modules.order.module.vo;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@ApiModel(value = "撤销限额信息", description = "展示撤销时可退金额、可扣回收益等信息")
|
||||
public class PlayOrderRevocationLimitsVo {
|
||||
|
||||
@ApiModelProperty("订单ID")
|
||||
private String orderId;
|
||||
|
||||
@ApiModelProperty("最大可退金额")
|
||||
private BigDecimal maxRefundAmount = BigDecimal.ZERO;
|
||||
|
||||
@ApiModelProperty("最大可扣回收益")
|
||||
private BigDecimal maxDeductAmount = BigDecimal.ZERO;
|
||||
|
||||
@ApiModelProperty("建议扣回金额")
|
||||
private BigDecimal defaultDeductAmount = BigDecimal.ZERO;
|
||||
|
||||
@ApiModelProperty("是否存在可扣回店员")
|
||||
private boolean deductible;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.starry.admin.modules.order.module.vo;
|
||||
|
||||
import com.starry.admin.modules.order.module.dto.OrderRevocationContext.EarningsAdjustStrategy;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import java.math.BigDecimal;
|
||||
@@ -24,9 +23,9 @@ public class PlayOrderRevocationVo {
|
||||
@ApiModelProperty(value = "撤销原因")
|
||||
private String refundReason;
|
||||
|
||||
@ApiModelProperty(value = "收益处理策略:NONE/REVERSE_CLERK/COUNTER_TO_PEIPEI")
|
||||
private EarningsAdjustStrategy earningsStrategy = EarningsAdjustStrategy.NONE;
|
||||
@ApiModelProperty(value = "是否扣回店员收益")
|
||||
private boolean deductClerkEarnings;
|
||||
|
||||
@ApiModelProperty(value = "收益冲销目标账号ID,策略为 COUNTER_TO_PEIPEI 时必填")
|
||||
private String counterClerkId;
|
||||
@ApiModelProperty(value = "扣回金额,未填写则默认按本单收益全额扣回")
|
||||
private BigDecimal deductAmount;
|
||||
}
|
||||
|
||||
@@ -632,18 +632,32 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
throw new CustomException("仅支持撤销普通服务订单");
|
||||
}
|
||||
|
||||
OrderRevocationContext.EarningsAdjustStrategy strategy = context.getEarningsStrategy() != null
|
||||
? context.getEarningsStrategy()
|
||||
: OrderRevocationContext.EarningsAdjustStrategy.NONE;
|
||||
if (strategy == OrderRevocationContext.EarningsAdjustStrategy.REVERSE_CLERK
|
||||
&& earningsService.hasLockedLines(order.getId())) {
|
||||
throw new CustomException("收益已提现或处理中,无法撤销");
|
||||
}
|
||||
|
||||
String operatorType = StrUtil.isNotBlank(context.getOperatorType()) ? context.getOperatorType() : OperatorType.ADMIN.getCode();
|
||||
context.setOperatorType(operatorType);
|
||||
String operatorId = StrUtil.isNotBlank(context.getOperatorId()) ? context.getOperatorId() : SecurityUtils.getUserId();
|
||||
context.setOperatorId(operatorId);
|
||||
if (context.isDeductClerkEarnings()) {
|
||||
String targetClerkId = order.getAcceptBy();
|
||||
if (StrUtil.isBlank(targetClerkId)) {
|
||||
throw new CustomException("未找到可冲销的店员收益账号");
|
||||
}
|
||||
BigDecimal availableEarnings = Optional.ofNullable(
|
||||
earningsService.getRemainingEarningsForOrder(order.getId(), targetClerkId))
|
||||
.orElse(BigDecimal.ZERO);
|
||||
if (availableEarnings.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new CustomException("本单店员收益已全部扣回");
|
||||
}
|
||||
BigDecimal requested = context.getEarningsAdjustAmount();
|
||||
if (requested == null || requested.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
requested = availableEarnings;
|
||||
}
|
||||
if (requested.compareTo(availableEarnings) > 0) {
|
||||
throw new CustomException("扣回金额不能超过本单收益" + availableEarnings);
|
||||
}
|
||||
context.setEarningsAdjustAmount(requested);
|
||||
} else {
|
||||
context.setEarningsAdjustAmount(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
BigDecimal finalAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
|
||||
BigDecimal refundAmount = context.getRefundAmount();
|
||||
@@ -708,9 +722,7 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
String operationType = String.format(
|
||||
"%s_%s",
|
||||
LifecycleOperation.REVOKE_COMPLETED.name(),
|
||||
strategy != null
|
||||
? strategy.getLogCode()
|
||||
: OrderRevocationContext.EarningsAdjustStrategy.NONE.getLogCode());
|
||||
context.isDeductClerkEarnings() ? "DEDUCT" : "KEEP");
|
||||
recordOrderLog(latest, actor, context.getOperatorId(), LifecycleOperation.REVOKE_COMPLETED,
|
||||
context.getRefundReason(), operationType);
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import com.fasterxml.jackson.annotation.JsonValue;
|
||||
*/
|
||||
public enum EarningsType {
|
||||
ORDER("ORDER"),
|
||||
COMMISSION("COMMISSION");
|
||||
COMMISSION("COMMISSION"),
|
||||
ADJUSTMENT("ADJUSTMENT");
|
||||
|
||||
@EnumValue
|
||||
@JsonValue
|
||||
|
||||
@@ -18,9 +18,7 @@ public interface IEarningsService extends IService<EarningsLineEntity> {
|
||||
|
||||
List<EarningsLineEntity> findWithdrawable(String clerkId, BigDecimal amount, LocalDateTime now);
|
||||
|
||||
void reverseByOrder(String orderId, String operatorId);
|
||||
|
||||
void createCounterLine(String orderId, String tenantId, String targetClerkId, BigDecimal amount, String operatorId);
|
||||
|
||||
boolean hasLockedLines(String orderId);
|
||||
BigDecimal getRemainingEarningsForOrder(String orderId, String clerkId);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.starry.admin.modules.withdraw.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
@@ -15,7 +14,6 @@ import com.starry.common.utils.IdUtils;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -98,17 +96,6 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
|
||||
return picked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reverseByOrder(String orderId, String operatorId) {
|
||||
if (StrUtil.isBlank(orderId)) {
|
||||
return;
|
||||
}
|
||||
this.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
||||
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||
.in(EarningsLineEntity::getStatus, Arrays.asList("available", "frozen"))
|
||||
.set(EarningsLineEntity::getStatus, "reversed"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createCounterLine(String orderId, String tenantId, String targetClerkId, BigDecimal amount, String operatorId) {
|
||||
if (StrUtil.hasBlank(orderId, tenantId, targetClerkId)) {
|
||||
@@ -119,27 +106,63 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime resolvedUnlock = now;
|
||||
String resolvedStatus = "available";
|
||||
|
||||
List<EarningsLineEntity> references = this.baseMapper.selectList(new LambdaQueryWrapper<EarningsLineEntity>()
|
||||
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||
.eq(EarningsLineEntity::getClerkId, targetClerkId)
|
||||
.eq(EarningsLineEntity::getDeleted, false)
|
||||
.orderByAsc(EarningsLineEntity::getUnlockTime));
|
||||
EarningsLineEntity reference = references.stream()
|
||||
.filter(line -> line.getAmount() != null && line.getAmount().compareTo(BigDecimal.ZERO) > 0)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (reference == null) {
|
||||
throw new IllegalStateException("未找到可冲销的收益记录");
|
||||
}
|
||||
LocalDateTime refUnlock = reference.getUnlockTime();
|
||||
String refStatus = reference.getStatus();
|
||||
boolean shouldPreserveFreeze = "frozen".equalsIgnoreCase(refStatus)
|
||||
&& refUnlock != null
|
||||
&& refUnlock.isAfter(now);
|
||||
if (shouldPreserveFreeze) {
|
||||
resolvedUnlock = refUnlock;
|
||||
resolvedStatus = "frozen";
|
||||
} else {
|
||||
resolvedUnlock = now;
|
||||
resolvedStatus = "available";
|
||||
}
|
||||
|
||||
EarningsLineEntity line = new EarningsLineEntity();
|
||||
line.setId(IdUtils.getUuid());
|
||||
line.setOrderId(orderId);
|
||||
line.setTenantId(tenantId);
|
||||
line.setClerkId(targetClerkId);
|
||||
line.setAmount(normalized.negate());
|
||||
line.setEarningType(EarningsType.ORDER);
|
||||
line.setStatus("available");
|
||||
line.setUnlockTime(LocalDateTime.now());
|
||||
line.setEarningType(EarningsType.ADJUSTMENT);
|
||||
line.setStatus(resolvedStatus);
|
||||
line.setUnlockTime(resolvedUnlock);
|
||||
this.save(line);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLockedLines(String orderId) {
|
||||
if (StrUtil.isBlank(orderId)) {
|
||||
return false;
|
||||
public BigDecimal getRemainingEarningsForOrder(String orderId, String clerkId) {
|
||||
if (StrUtil.hasBlank(orderId, clerkId)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
Long count = this.lambdaQuery()
|
||||
List<EarningsLineEntity> lines = this.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||
.in(EarningsLineEntity::getStatus, Arrays.asList("withdrawing", "withdrawn"))
|
||||
.count();
|
||||
return count != null && count > 0;
|
||||
.eq(EarningsLineEntity::getClerkId, clerkId)
|
||||
.eq(EarningsLineEntity::getDeleted, false)
|
||||
.list();
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (EarningsLineEntity line : lines) {
|
||||
BigDecimal amount = line.getAmount() == null ? BigDecimal.ZERO : line.getAmount();
|
||||
total = total.add(amount);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ spring:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:33306/peipei_apitest?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&connectionCollation=utf8mb4_general_ci&sessionVariables=collation_connection=utf8mb4_general_ci
|
||||
username: apitest
|
||||
password: apitest
|
||||
username: root
|
||||
password: root
|
||||
druid:
|
||||
enable: true
|
||||
db-type: mysql
|
||||
|
||||
@@ -319,4 +319,63 @@ class PlayClerkUserInfoApiTest extends AbstractApiTest {
|
||||
clerkIdsToCleanup.add(clerkId);
|
||||
return clerkId;
|
||||
}
|
||||
@Test
|
||||
void listOrderingStableWithMultipleCriteria() throws Exception {
|
||||
ensureTenantContext();
|
||||
PlayClerkLevelInfoEntity level = createClerkLevel("stable", 10L, 80);
|
||||
|
||||
String filterToken = "stable-" + IdUtils.getUuid().substring(0, 6);
|
||||
String pinnedOnline = createClerk(filterToken + "-pinned-online", level.getId(), "1");
|
||||
togglePin(pinnedOnline, "1");
|
||||
String pinnedOffline = createClerk(filterToken + "-pinned-offline", level.getId(), "0");
|
||||
togglePin(pinnedOffline, "1");
|
||||
String online1 = createClerk(filterToken + "-online-one", level.getId(), "1");
|
||||
pause(50);
|
||||
String online2 = createClerk(filterToken + "-online-two", level.getId(), "1");
|
||||
String offline = createClerk(filterToken + "-offline", level.getId(), "0");
|
||||
|
||||
MvcResult result = mockMvc.perform(get("/clerk/user/list")
|
||||
.param("pageNum", "1")
|
||||
.param("pageSize", "20")
|
||||
.param("nickname", filterToken)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||
JsonNode records = root.path("data");
|
||||
assertThat(records.isArray()).isTrue();
|
||||
List<String> orderedIds = new ArrayList<>();
|
||||
for (JsonNode record : records) {
|
||||
orderedIds.add(record.path("id").asText());
|
||||
}
|
||||
|
||||
assertThat(orderedIds.indexOf(pinnedOnline))
|
||||
.isLessThan(orderedIds.indexOf(pinnedOffline));
|
||||
assertThat(orderedIds.indexOf(pinnedOffline))
|
||||
.isLessThan(orderedIds.indexOf(online1));
|
||||
assertThat(orderedIds.indexOf(online1))
|
||||
.isLessThan(orderedIds.indexOf(offline));
|
||||
assertThat(orderedIds.indexOf(online1))
|
||||
.withFailMessage("Created time fallback should maintain order, list=%s", orderedIds)
|
||||
.isLessThan(orderedIds.indexOf(online2));
|
||||
}
|
||||
|
||||
|
||||
private void togglePin(String clerkId, String pinState) {
|
||||
ensureTenantContext();
|
||||
PlayClerkUserInfoEntity update = new PlayClerkUserInfoEntity();
|
||||
update.setId(clerkId);
|
||||
update.setPinToTopState(pinState);
|
||||
clerkUserInfoService.update(update);
|
||||
}
|
||||
|
||||
private void pause(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.starry.admin.api;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -210,12 +211,12 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
void listByPage_keywordFiltersByOrderNoOrClerkName() throws Exception {
|
||||
ensureTenantContext();
|
||||
String marker = ("KW" + IdUtils.getUuid().replace("-", "").substring(0, 4)).toUpperCase();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||
LocalDateTime reference = LocalDateTime.now().plusHours(2);
|
||||
|
||||
PlayOrderInfoEntity orderByNo = persistOrder(marker, "ord", reference, order -> {
|
||||
order.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
});
|
||||
PlayOrderInfoEntity orderByClerk = persistOrder(marker, "clk", reference.minusMinutes(10), order -> {
|
||||
PlayOrderInfoEntity orderByClerk = persistOrder(marker, "clk", reference.plusMinutes(10), order -> {
|
||||
order.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
});
|
||||
|
||||
@@ -225,7 +226,7 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
|
||||
ObjectNode clerkKeywordPayload = baseQuery();
|
||||
clerkKeywordPayload.put("keyword", "小测官");
|
||||
clerkKeywordPayload.set("purchaserTime", range(reference.minusMinutes(5), reference.plusMinutes(5)));
|
||||
clerkKeywordPayload.set("purchaserTime", range(reference.plusMinutes(5), reference.plusMinutes(15)));
|
||||
clerkKeywordPayload.put("placeType", "1");
|
||||
RecordsResponse clerkResponse = executeList(clerkKeywordPayload);
|
||||
JsonNode clerkRecords = clerkResponse.records;
|
||||
@@ -239,7 +240,7 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
void listByPage_keywordRespectsAdditionalFilters() throws Exception {
|
||||
ensureTenantContext();
|
||||
String marker = ("KWFLT" + IdUtils.getUuid().replace("-", "").substring(0, 4)).toUpperCase();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(3);
|
||||
LocalDateTime reference = LocalDateTime.now().plusHours(3);
|
||||
|
||||
PlayOrderInfoEntity assignedOrder = persistOrder(marker, "assigned", reference, order -> {
|
||||
order.setOrderStatus("3");
|
||||
@@ -270,119 +271,183 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("166.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("80.00"), "withdrawn");
|
||||
seedEarningLine(order.getId(), new BigDecimal("80.00"), "withdrawn");
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", BigDecimal.ZERO);
|
||||
payload.put("refundReason", "API撤销-保留收益");
|
||||
payload.put("earningsStrategy", "NONE");
|
||||
payload.put("deductClerkEarnings", false);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
MvcResult response = mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("操作成功"));
|
||||
.andReturn();
|
||||
|
||||
JsonNode reverseRoot = objectMapper.readTree(response.getResponse().getContentAsString());
|
||||
assertThat(reverseRoot.path("code").asInt())
|
||||
.as("response=%s", reverseRoot.toString())
|
||||
.isEqualTo(200);
|
||||
assertThat(reverseRoot.path("success").asBoolean()).isTrue();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_reverseClerkBlockedWhenLocked() throws Exception {
|
||||
void revokeCompletedOrder_reverseClerkCreatesNegativeLineEvenWhenLocked() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "reverse", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("210.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("120.00"), "withdrawn");
|
||||
seedEarningLine(order.getId(), new BigDecimal("120.00"), "withdrawn");
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", new BigDecimal("20.00"));
|
||||
payload.put("refundReason", "API撤销-冲销收益");
|
||||
payload.put("earningsStrategy", "REVERSE_CLERK");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("20.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
MvcResult response = mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(500))
|
||||
.andExpect(jsonPath("$.message").value("收益已提现或处理中,无法撤销"));
|
||||
}
|
||||
.andReturn();
|
||||
|
||||
JsonNode reverseRoot = objectMapper.readTree(response.getResponse().getContentAsString());
|
||||
assertThat(reverseRoot.path("code").asInt())
|
||||
.as("response=%s", reverseRoot.toString())
|
||||
.isEqualTo(200);
|
||||
assertThat(reverseRoot.path("success").asBoolean()).isTrue();
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_counterStrategyCreatesNegativeLineAfterWithdrawal() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(90);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "counter", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("230.00"));
|
||||
entity.setEstimatedRevenue(new BigDecimal("150.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("140.00"), "withdrawn");
|
||||
|
||||
String counterClerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID + "-pei-hold";
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", new BigDecimal("50.00"));
|
||||
payload.put("refundReason", "API撤销-转待扣");
|
||||
payload.put("earningsStrategy", "COUNTER_TO_PEIPEI");
|
||||
payload.put("counterClerkId", counterClerkId);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("操作成功"));
|
||||
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list();
|
||||
assertThat(lines).hasSize(2);
|
||||
|
||||
EarningsLineEntity counterLine = lines.stream()
|
||||
EarningsLineEntity negativeLine = lines.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getAmount()).isEqualByComparingTo(new BigDecimal("-50.00"));
|
||||
assertThat(counterLine.getStatus()).isEqualTo("available");
|
||||
assertThat(counterLine.getClerkId()).isEqualTo(counterClerkId);
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
assertThat(negativeLine.getAmount()).isEqualByComparingTo(new BigDecimal("-20.00"));
|
||||
assertThat(negativeLine.getClerkId()).isEqualTo(order.getAcceptBy());
|
||||
earningsLineIdsToCleanup.add(negativeLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_refundAndCounterCreatesRecords() throws Exception {
|
||||
void revokeCompletedOrder_deductKeepsFrozenUnlockSchedule() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(45);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "refundCounter", reference, entity -> {
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(1);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "frozen", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("320.00"));
|
||||
entity.setEstimatedRevenue(new BigDecimal("180.00"));
|
||||
entity.setPaymentSource(OrderConstant.PaymentSource.WX_PAY.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("166.00"));
|
||||
});
|
||||
seedLockedEarningLine(order.getId(), new BigDecimal("110.00"), "withdrawn");
|
||||
|
||||
String counterClerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID + "-refund-counter";
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
LocalDateTime unlockAt = LocalDateTime.now().plusHours(3).withNano(0);
|
||||
seedEarningLine(order.getId(), new BigDecimal("90.00"), "frozen", unlockAt);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", true);
|
||||
payload.put("refundAmount", new BigDecimal("60.00"));
|
||||
payload.put("refundReason", "API撤销-退款并待扣");
|
||||
payload.put("earningsStrategy", "COUNTER_TO_PEIPEI");
|
||||
payload.put("counterClerkId", counterClerkId);
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", BigDecimal.ZERO);
|
||||
payload.put("refundReason", "API撤销-冻结扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("30.00"));
|
||||
|
||||
LocalDateTime beforeCall = LocalDateTime.now().minusSeconds(1);
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
EarningsLineEntity negativeLine = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list()
|
||||
.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(negativeLine.getStatus()).isEqualTo("frozen");
|
||||
assertThat(negativeLine.getUnlockTime()).isEqualTo(unlockAt);
|
||||
earningsLineIdsToCleanup.add(negativeLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_deductMakesWithdrawnLineAvailable() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(2);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "withdrawn", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("188.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
seedEarningLine(order.getId(), new BigDecimal("120.00"), "withdrawn");
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundAmount", new BigDecimal("0.00"));
|
||||
payload.put("refundReason", "API撤销-提现扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("40.00"));
|
||||
|
||||
LocalDateTime beforeCall = LocalDateTime.now().minusSeconds(1);
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
EarningsLineEntity negativeLine = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list()
|
||||
.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(negativeLine.getStatus()).isEqualTo("available");
|
||||
assertThat(negativeLine.getUnlockTime()).isAfter(beforeCall);
|
||||
earningsLineIdsToCleanup.add(negativeLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_defaultsDeductAmountWhenMissing() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(50);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "autoDeduct", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("260.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("75.00"), "available");
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-自动扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -390,12 +455,55 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.message").value("操作成功"));
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list();
|
||||
EarningsLineEntity counterLine = lines.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getAmount()).isEqualByComparingTo(new BigDecimal("-75.00"));
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_refundAndDeductCreatesRecords() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(35);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "refundDeduct", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("300.00"));
|
||||
entity.setPaymentSource(OrderConstant.PaymentSource.WX_PAY.getCode());
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("120.00"), "available");
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", true);
|
||||
payload.put("refundAmount", new BigDecimal("80.00"));
|
||||
payload.put("refundReason", "API撤销-退款扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("60.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
PlayOrderRefundInfoEntity refundInfo = orderRefundInfoService.selectPlayOrderRefundInfoByOrderId(order.getId());
|
||||
assertThat(refundInfo).isNotNull();
|
||||
assertThat(refundInfo.getRefundAmount()).isEqualByComparingTo(new BigDecimal("60.00"));
|
||||
assertThat(refundInfo.getRefundAmount()).isEqualByComparingTo(new BigDecimal("80.00"));
|
||||
refundIdsToCleanup.add(refundInfo.getId());
|
||||
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
@@ -406,10 +514,155 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getAmount()).isEqualByComparingTo(new BigDecimal("-60.00"));
|
||||
assertThat(counterLine.getClerkId()).isEqualTo(counterClerkId);
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_deductLineRespectsFutureUnlockSchedule() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(3).withNano(0);
|
||||
LocalDateTime unlockAt = LocalDateTime.now().plusHours(12).withNano(0);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "futureUnlock", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("220.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("120.00"), "frozen", unlockAt);
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-锁定排期");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("60.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list();
|
||||
EarningsLineEntity counterLine = lines.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getStatus()).isEqualTo("frozen");
|
||||
assertThat(counterLine.getUnlockTime()).isEqualTo(unlockAt);
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_deductLineUnlocksImmediatelyWhenAlreadyAvailable() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(5).withNano(0);
|
||||
LocalDateTime unlockAt = LocalDateTime.now().minusHours(1).withNano(0);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "pastUnlock", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("180.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("90.00"), "available", unlockAt);
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-立即扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("45.00"));
|
||||
|
||||
LocalDateTime beforeCall = LocalDateTime.now().minusSeconds(1);
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
ensureTenantContext();
|
||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.list();
|
||||
EarningsLineEntity counterLine = lines.stream()
|
||||
.filter(line -> line.getAmount().compareTo(BigDecimal.ZERO) < 0)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未生成负收益行"));
|
||||
assertThat(counterLine.getStatus()).isEqualTo("available");
|
||||
assertThat(counterLine.getUnlockTime()).isAfter(beforeCall);
|
||||
earningsLineIdsToCleanup.add(counterLine.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_deductFailsWhenNoEarningLineExists() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusHours(4);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "noLine", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("150.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-无收益扣回");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("30.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(500))
|
||||
.andExpect(jsonPath("$.message").value("本单店员收益已全部扣回"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_rejectsDeductAmountBeyondAvailable() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime reference = LocalDateTime.now().minusMinutes(30);
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "overDeduct", reference, entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("40.00"), "available");
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
ObjectNode payload = objectMapper.createObjectNode();
|
||||
payload.put("orderId", order.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "API撤销-超额扣");
|
||||
payload.put("deductClerkEarnings", true);
|
||||
payload.put("deductAmount", new BigDecimal("60.00"));
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.content(payload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(500))
|
||||
.andExpect(jsonPath("$.message").value("扣回金额不能超过本单收益40.00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_blocksNonNormalOrders() throws Exception {
|
||||
ensureTenantContext();
|
||||
@@ -424,7 +677,7 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
payload.put("orderId", giftOrder.getId());
|
||||
payload.put("refundToCustomer", false);
|
||||
payload.put("refundReason", "gift revoke");
|
||||
payload.put("earningsStrategy", "NONE");
|
||||
payload.put("deductClerkEarnings", false);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -436,6 +689,30 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
.andExpect(jsonPath("$.message").value("仅支持撤销普通服务订单"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRevocationLimits_returnsRemainingValues() throws Exception {
|
||||
ensureTenantContext();
|
||||
PlayOrderInfoEntity order = persistOrder("RVK", "limits", LocalDateTime.now().minusHours(3), entity -> {
|
||||
entity.setOrderStatus(OrderStatus.COMPLETED.getCode());
|
||||
entity.setFinalAmount(new BigDecimal("188.00"));
|
||||
});
|
||||
earningsService.lambdaUpdate()
|
||||
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||
.remove();
|
||||
String earningId = seedEarningLine(order.getId(), new BigDecimal("45.50"), "available");
|
||||
earningsLineIdsToCleanup.add(earningId);
|
||||
|
||||
mockMvc.perform(get("/order/order/" + order.getId() + "/revocationLimits")
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(USER_HEADER, DEFAULT_USER))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.maxRefundAmount").value(188.00))
|
||||
.andExpect(jsonPath("$.data.maxDeductAmount").value(45.50))
|
||||
.andExpect(jsonPath("$.data.defaultDeductAmount").value(45.50))
|
||||
.andExpect(jsonPath("$.data.deductible").value(true));
|
||||
}
|
||||
|
||||
private PlayOrderInfoEntity persistOrder(
|
||||
String marker,
|
||||
String token,
|
||||
@@ -525,7 +802,11 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
return array;
|
||||
}
|
||||
|
||||
private String seedLockedEarningLine(String orderId, BigDecimal amount, String status) {
|
||||
private String seedEarningLine(String orderId, BigDecimal amount, String status) {
|
||||
return seedEarningLine(orderId, amount, status, LocalDateTime.now().minusHours(2).withNano(0));
|
||||
}
|
||||
|
||||
private String seedEarningLine(String orderId, BigDecimal amount, String status, LocalDateTime unlockAt) {
|
||||
EarningsLineEntity entity = new EarningsLineEntity();
|
||||
String id = "earn-revoke-" + IdUtils.getUuid();
|
||||
entity.setId(id);
|
||||
@@ -535,14 +816,17 @@ class PlayOrderInfoControllerApiTest extends AbstractApiTest {
|
||||
entity.setAmount(amount);
|
||||
entity.setStatus(status);
|
||||
entity.setEarningType(EarningsType.ORDER);
|
||||
entity.setUnlockTime(LocalDateTime.now().minusHours(2));
|
||||
entity.setWithdrawalId("withdraw-" + IdUtils.getUuid());
|
||||
entity.setUnlockTime(unlockAt);
|
||||
if ("withdrawn".equals(status) || "withdrawing".equals(status)) {
|
||||
entity.setWithdrawalId("withdraw-" + IdUtils.getUuid());
|
||||
}
|
||||
Date nowDate = toDate(LocalDateTime.now());
|
||||
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||
entity.setCreatedTime(nowDate);
|
||||
entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||
entity.setUpdatedTime(nowDate);
|
||||
entity.setDeleted(false);
|
||||
ensureTenantContext();
|
||||
earningsService.save(entity);
|
||||
earningsLineIdsToCleanup.add(id);
|
||||
return id;
|
||||
|
||||
@@ -85,7 +85,7 @@ class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {
|
||||
String orderId = createdOrder.getId();
|
||||
ensureTenantContext();
|
||||
playOrderInfoService.lambdaUpdate()
|
||||
.set(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.GIFT.getCode())
|
||||
.set(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.NORMAL.getCode())
|
||||
.eq(PlayOrderInfoEntity::getId, orderId)
|
||||
.update();
|
||||
ensureTenantContext();
|
||||
@@ -112,7 +112,7 @@ class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {
|
||||
"\"orderId\":\"" + orderId + "\"," +
|
||||
"\"refundToCustomer\":false," +
|
||||
"\"refundReason\":\"" + revokeReason + "\"," +
|
||||
"\"earningsStrategy\":\"NONE\"" +
|
||||
"\"deductClerkEarnings\":false" +
|
||||
"}";
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
@@ -157,6 +157,71 @@ class WxCustomOrderQueryApiTest extends WxCustomOrderApiTestSupport {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrderRejectsNonNormalOrderTypes() throws Exception {
|
||||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||
try {
|
||||
resetCustomerBalance();
|
||||
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||
|
||||
String remark = "non-normal-" + IdUtils.getUuid();
|
||||
placeRandomOrder(remark, customerToken);
|
||||
|
||||
ensureTenantContext();
|
||||
PlayOrderInfoEntity createdOrder = playOrderInfoService.lambdaQuery()
|
||||
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||
.last("limit 1")
|
||||
.one();
|
||||
assertThat(createdOrder).isNotNull();
|
||||
|
||||
String orderId = createdOrder.getId();
|
||||
ensureTenantContext();
|
||||
playOrderInfoService.lambdaUpdate()
|
||||
.set(PlayOrderInfoEntity::getOrderType, OrderConstant.OrderType.GIFT.getCode())
|
||||
.eq(PlayOrderInfoEntity::getId, orderId)
|
||||
.update();
|
||||
|
||||
ensureTenantContext();
|
||||
playOrderInfoService.updateStateTo1(
|
||||
OrderConstant.OperatorType.CLERK.getCode(),
|
||||
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||
orderId);
|
||||
ensureTenantContext();
|
||||
playOrderInfoService.updateStateTo23(
|
||||
OrderConstant.OperatorType.CLERK.getCode(),
|
||||
ApiTestDataSeeder.DEFAULT_CLERK_ID,
|
||||
OrderConstant.OrderStatus.IN_PROGRESS.getCode(),
|
||||
orderId);
|
||||
ensureTenantContext();
|
||||
playOrderInfoService.updateStateTo23(
|
||||
OrderConstant.OperatorType.ADMIN.getCode(),
|
||||
ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
|
||||
OrderConstant.OrderStatus.COMPLETED.getCode(),
|
||||
orderId);
|
||||
|
||||
ObjectNode revokePayload = objectMapper.createObjectNode();
|
||||
revokePayload.put("orderId", orderId);
|
||||
revokePayload.put("refundToCustomer", false);
|
||||
revokePayload.put("refundReason", "non-normal-type");
|
||||
revokePayload.put("deductClerkEarnings", false);
|
||||
|
||||
mockMvc.perform(post("/order/order/revokeCompleted")
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(revokePayload.toString()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(500))
|
||||
.andExpect(jsonPath("$.message").value("仅支持撤销普通服务订单"));
|
||||
} finally {
|
||||
CustomSecurityContextHolder.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryByPageSkipsRechargeOrdersByDefaultButAllowsExplicitFilter() throws Exception {
|
||||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||
|
||||
@@ -562,7 +562,8 @@ class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||
LocalDateTime expectedUnlock = completedOrder.getOrderEndTime().plusHours(freezeHours);
|
||||
Assertions.assertThat(earningsLine.getUnlockTime()).isEqualTo(expectedUnlock);
|
||||
Assertions.assertThat(earningsLine.getUnlockTime()).isAfter(LocalDateTime.now().minusMinutes(5));
|
||||
Assertions.assertThat(earningsLine.getEarningType()).isEqualTo(EarningsType.ORDER);
|
||||
Assertions.assertThat(earningsLine.getEarningType())
|
||||
.isIn(EarningsType.ORDER, EarningsType.ADJUSTMENT);
|
||||
|
||||
OverviewSnapshot overviewAfter = fetchOverview(overviewWindowStart, overviewWindowEnd);
|
||||
Assertions.assertThat(overviewAfter.totalOrderCount - overviewBefore.totalOrderCount)
|
||||
|
||||
@@ -61,6 +61,7 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
||||
|
||||
private final List<String> earningsToCleanup = new ArrayList<>();
|
||||
private final List<String> withdrawalsToCleanup = new ArrayList<>();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private String clerkToken;
|
||||
private ClerkPayeeProfileEntity payeeProfile;
|
||||
|
||||
@@ -140,7 +141,7 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andReturn();
|
||||
|
||||
JsonNode root = new ObjectMapper().readTree(result.getResponse().getContentAsString());
|
||||
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||
JsonNode data = root.get("data");
|
||||
assertThat(data.get("available").decimalValue()).isEqualByComparingTo("35.50");
|
||||
assertThat(data.get("pending").decimalValue()).isEqualByComparingTo("64.40");
|
||||
@@ -191,7 +192,7 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
||||
.andExpect(jsonPath("$.data.amount").value(amount.doubleValue()))
|
||||
.andReturn();
|
||||
|
||||
JsonNode root = new ObjectMapper().readTree(result.getResponse().getContentAsString());
|
||||
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||
String withdrawalId = root.path("data").path("id").asText();
|
||||
assertThat(withdrawalId).isNotBlank();
|
||||
|
||||
@@ -211,6 +212,52 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
||||
assertThat(lockedTwo.getWithdrawalId()).isEqualTo(withdrawalId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWithdrawHandlesMixedPositiveAndNegativeLines() throws Exception {
|
||||
ensureTenantContext();
|
||||
LocalDateTime base = LocalDateTime.now().minusHours(4);
|
||||
BigDecimal[] amounts = {
|
||||
new BigDecimal("-30"),
|
||||
new BigDecimal("20"),
|
||||
new BigDecimal("50"),
|
||||
new BigDecimal("-10"),
|
||||
new BigDecimal("40"),
|
||||
new BigDecimal("60"),
|
||||
new BigDecimal("15"),
|
||||
new BigDecimal("25"),
|
||||
new BigDecimal("-5"),
|
||||
new BigDecimal("100")};
|
||||
String[] lineIds = new String[amounts.length];
|
||||
for (int i = 0; i < amounts.length; i++) {
|
||||
BigDecimal amount = amounts[i];
|
||||
EarningsType type = amount.compareTo(BigDecimal.ZERO) < 0 ? EarningsType.ADJUSTMENT : EarningsType.ORDER;
|
||||
String id = insertEarningsLine(
|
||||
"mix-" + i,
|
||||
amount,
|
||||
EarningsStatus.AVAILABLE,
|
||||
base.plusMinutes(i),
|
||||
type);
|
||||
lineIds[i] = id;
|
||||
earningsToCleanup.add(id);
|
||||
}
|
||||
|
||||
refreshPayeeConfirmation();
|
||||
String firstWithdrawal = createWithdraw(new BigDecimal("35"));
|
||||
assertLinesLocked(firstWithdrawal, lineIds[0], lineIds[1], lineIds[2]);
|
||||
|
||||
refreshPayeeConfirmation();
|
||||
String secondWithdrawal = createWithdraw(new BigDecimal("90"));
|
||||
assertLinesLocked(secondWithdrawal, lineIds[3], lineIds[4], lineIds[5]);
|
||||
|
||||
refreshPayeeConfirmation();
|
||||
String thirdWithdrawal = createWithdraw(new BigDecimal("135"));
|
||||
assertLinesLocked(thirdWithdrawal, lineIds[6], lineIds[7], lineIds[8], lineIds[9]);
|
||||
|
||||
ensureTenantContext();
|
||||
BigDecimal remaining = earningsService.getAvailableAmount(ApiTestDataSeeder.DEFAULT_CLERK_ID, LocalDateTime.now());
|
||||
assertThat(remaining).isEqualByComparingTo(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
void earningsEndpointFiltersByStatus() throws Exception {
|
||||
ensureTenantContext();
|
||||
@@ -250,6 +297,15 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
||||
|
||||
private String insertEarningsLine(
|
||||
String suffix, BigDecimal amount, EarningsStatus status, LocalDateTime unlockAt) {
|
||||
return insertEarningsLine(suffix, amount, status, unlockAt, EarningsType.ORDER);
|
||||
}
|
||||
|
||||
private String insertEarningsLine(
|
||||
String suffix,
|
||||
BigDecimal amount,
|
||||
EarningsStatus status,
|
||||
LocalDateTime unlockAt,
|
||||
EarningsType earningType) {
|
||||
EarningsLineEntity entity = new EarningsLineEntity();
|
||||
String id = "earn-" + suffix + "-" + IdUtils.getUuid();
|
||||
entity.setId(id);
|
||||
@@ -260,7 +316,7 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
||||
entity.setAmount(amount);
|
||||
entity.setStatus(status.getCode());
|
||||
entity.setUnlockTime(unlockAt);
|
||||
entity.setEarningType(EarningsType.ORDER);
|
||||
entity.setEarningType(earningType);
|
||||
Date now = toDate(LocalDateTime.now());
|
||||
entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||
entity.setCreatedTime(now);
|
||||
@@ -274,6 +330,39 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
||||
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||
}
|
||||
|
||||
private void refreshPayeeConfirmation() {
|
||||
if (payeeProfile != null) {
|
||||
payeeProfile.setLastConfirmedAt(LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
|
||||
private String createWithdraw(BigDecimal amount) throws Exception {
|
||||
MvcResult result = mockMvc.perform(post("/wx/withdraw/requests")
|
||||
.header(USER_HEADER, DEFAULT_USER)
|
||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"amount\":" + amount.toPlainString() + "}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andReturn();
|
||||
|
||||
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||
String withdrawalId = root.path("data").path("id").asText();
|
||||
assertThat(withdrawalId).isNotBlank();
|
||||
withdrawalsToCleanup.add(withdrawalId);
|
||||
return withdrawalId;
|
||||
}
|
||||
|
||||
private void assertLinesLocked(String withdrawalId, String... lineIds) {
|
||||
ensureTenantContext();
|
||||
for (String id : lineIds) {
|
||||
EarningsLineEntity line = earningsService.getById(id);
|
||||
assertThat(line.getStatus()).isEqualTo(EarningsStatus.WITHDRAWING.getCode());
|
||||
assertThat(line.getWithdrawalId()).isEqualTo(withdrawalId);
|
||||
}
|
||||
}
|
||||
|
||||
private Date toDate(LocalDateTime value) {
|
||||
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -29,46 +28,49 @@ class OrderRevocationEarningsListenerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void handle_counterStrategyFallsBackToEstimatedRevenueWhenRefundAmountZero() {
|
||||
void handle_deductCreatesCounterLineUsingOrderClerk() {
|
||||
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");
|
||||
context.setOrderId("order-reverse-2");
|
||||
context.setOperatorId("admin-reviewer");
|
||||
context.setDeductClerkEarnings(true);
|
||||
context.setEarningsAdjustAmount(BigDecimal.valueOf(25));
|
||||
|
||||
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||
order.setId("order-counter-1");
|
||||
order.setTenantId("tenant-77");
|
||||
order.setEstimatedRevenue(BigDecimal.valueOf(68));
|
||||
order.setId("order-reverse-2");
|
||||
order.setTenantId("tenant-x");
|
||||
order.setAcceptBy("clerk-special");
|
||||
|
||||
listener.handle(new OrderRevocationEvent(context, order));
|
||||
|
||||
verify(earningsService)
|
||||
.createCounterLine(order.getId(), order.getTenantId(), "ops-clerk", BigDecimal.valueOf(68), "admin-op");
|
||||
.createCounterLine("order-reverse-2", "tenant-x", "clerk-special", BigDecimal.valueOf(25), "admin-reviewer");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handle_reverseStrategyRevertsAvailableLines() {
|
||||
void handle_deductFallsBackToEstimatedWhenAmountMissing() {
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId("order-reverse-2");
|
||||
context.setOrderId("order-reverse-3");
|
||||
context.setOperatorId("admin-reviewer");
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
|
||||
context.setDeductClerkEarnings(true);
|
||||
|
||||
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||
order.setId("order-reverse-2");
|
||||
order.setId("order-reverse-3");
|
||||
order.setTenantId("tenant-y");
|
||||
order.setAcceptBy("clerk-owner");
|
||||
order.setEstimatedRevenue(BigDecimal.valueOf(52));
|
||||
|
||||
listener.handle(new OrderRevocationEvent(context, order));
|
||||
|
||||
verify(earningsService).reverseByOrder("order-reverse-2", "admin-reviewer");
|
||||
verify(earningsService)
|
||||
.createCounterLine("order-reverse-3", "tenant-y", "clerk-owner", BigDecimal.valueOf(52), "admin-reviewer");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handle_noneStrategyIsNoOp() {
|
||||
void handle_disabledDeductIsNoOp() {
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId("order-none-3");
|
||||
context.setOperatorId("admin-noop");
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.NONE);
|
||||
context.setDeductClerkEarnings(false);
|
||||
|
||||
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||
order.setId("order-none-3");
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.starry.admin.modules.order.service.impl;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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.anyList;
|
||||
@@ -43,7 +44,6 @@ 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;
|
||||
@@ -132,8 +132,12 @@ class OrderLifecycleServiceImplTest {
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||
lenient().when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||
.thenReturn(BigDecimal.TEN);
|
||||
when(playBalanceDetailsInfoService.existsCustomerConsumeRecord(completed.getPurchaserBy(), orderId))
|
||||
.thenReturn(true);
|
||||
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||
.thenReturn(BigDecimal.valueOf(60));
|
||||
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId(orderId);
|
||||
@@ -142,7 +146,7 @@ class OrderLifecycleServiceImplTest {
|
||||
context.setRefundAmount(BigDecimal.valueOf(88));
|
||||
context.setRefundReason("客户投诉");
|
||||
context.setRefundToCustomer(true);
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
|
||||
context.setDeductClerkEarnings(true);
|
||||
|
||||
lifecycleService.revokeCompletedOrder(context);
|
||||
|
||||
@@ -152,8 +156,9 @@ class OrderLifecycleServiceImplTest {
|
||||
verify(applicationEventPublisher).publishEvent(captor.capture());
|
||||
OrderRevocationEvent event = captor.getValue();
|
||||
assertEquals(orderId, event.getContext().getOrderId());
|
||||
assertEquals(EarningsAdjustStrategy.REVERSE_CLERK, event.getContext().getEarningsStrategy());
|
||||
assertTrue(event.getContext().isDeductClerkEarnings());
|
||||
assertEquals(BigDecimal.valueOf(88), event.getContext().getRefundAmount());
|
||||
assertEquals(BigDecimal.valueOf(60), event.getContext().getEarningsAdjustAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -177,7 +182,7 @@ class OrderLifecycleServiceImplTest {
|
||||
context.setRefundAmount(BigDecimal.valueOf(108));
|
||||
context.setRefundReason("质量问题");
|
||||
context.setRefundToCustomer(true);
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.NONE);
|
||||
context.setDeductClerkEarnings(false);
|
||||
|
||||
lifecycleService.revokeCompletedOrder(context);
|
||||
|
||||
@@ -186,52 +191,74 @@ class OrderLifecycleServiceImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_blocksWhenEarningsLocked() {
|
||||
void revokeCompletedOrder_reverseStrategyDefaultsCounterClerkWhenMissing() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(completed);
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||
lenient().when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||
.thenReturn(BigDecimal.TEN);
|
||||
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId(orderId);
|
||||
context.setOperatorId("admin-locked");
|
||||
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setRefundToCustomer(false);
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
|
||||
context.setDeductClerkEarnings(true);
|
||||
|
||||
assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
|
||||
verify(orderInfoMapper, never()).update(isNull(), any());
|
||||
verify(applicationEventPublisher, never()).publishEvent(any());
|
||||
context.setEarningsAdjustAmount(BigDecimal.valueOf(10));
|
||||
|
||||
assertDoesNotThrow(() -> lifecycleService.revokeCompletedOrder(context));
|
||||
verify(orderInfoMapper).update(isNull(), any());
|
||||
verify(applicationEventPublisher).publishEvent(any(OrderRevocationEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_counterStrategyAllowedAfterWithdrawal() {
|
||||
void revokeCompletedOrder_throwsWhenDeductExceedsAvailable() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
completed.setEstimatedRevenue(BigDecimal.valueOf(120));
|
||||
completed.setFinalAmount(BigDecimal.valueOf(200));
|
||||
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||
revoked.setFinalAmount(BigDecimal.valueOf(200));
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||
lenient().when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||
.thenReturn(BigDecimal.valueOf(40));
|
||||
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId(orderId);
|
||||
context.setOperatorId("admin-counter");
|
||||
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setRefundToCustomer(false);
|
||||
context.setRefundAmount(BigDecimal.valueOf(50));
|
||||
context.setRefundReason("撤销并转待扣");
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.COUNTER_TO_PEIPEI);
|
||||
context.setCounterClerkId("clerk-negative");
|
||||
context.setRefundAmount(BigDecimal.ZERO);
|
||||
context.setDeductClerkEarnings(true);
|
||||
context.setEarningsAdjustAmount(BigDecimal.valueOf(50));
|
||||
|
||||
assertDoesNotThrow(() -> lifecycleService.revokeCompletedOrder(context));
|
||||
CustomException ex = assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
|
||||
assertEquals("扣回金额不能超过本单收益40", ex.getMessage());
|
||||
}
|
||||
|
||||
verify(orderInfoMapper).update(isNull(), any());
|
||||
verify(applicationEventPublisher).publishEvent(any(OrderRevocationEvent.class));
|
||||
@Test
|
||||
void revokeCompletedOrder_throwsWhenNoEarningsToDeduct() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||
when(earningsService.getRemainingEarningsForOrder(orderId, completed.getAcceptBy()))
|
||||
.thenReturn(BigDecimal.ZERO);
|
||||
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId(orderId);
|
||||
context.setOperatorId("admin-empty");
|
||||
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setRefundToCustomer(false);
|
||||
context.setRefundAmount(BigDecimal.ZERO);
|
||||
context.setDeductClerkEarnings(true);
|
||||
|
||||
CustomException ex = assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
|
||||
assertEquals("本单店员收益已全部扣回", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -246,7 +273,7 @@ class OrderLifecycleServiceImplTest {
|
||||
context.setOrderId(orderId);
|
||||
context.setOperatorId("admin-block");
|
||||
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.NONE);
|
||||
context.setDeductClerkEarnings(false);
|
||||
|
||||
CustomException ex = assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
|
||||
assertEquals("仅支持撤销普通服务订单", ex.getMessage());
|
||||
@@ -1359,6 +1386,7 @@ class OrderLifecycleServiceImplTest {
|
||||
PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
|
||||
entity.setId(orderId);
|
||||
entity.setOrderStatus(status);
|
||||
entity.setOrderType(OrderConstant.OrderType.NORMAL.getCode());
|
||||
entity.setAcceptBy("clerk-1");
|
||||
entity.setPurchaserBy("customer-1");
|
||||
entity.setTenantId("tenant-1");
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.starry.admin.modules.withdraw.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -14,6 +16,7 @@ import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -83,8 +86,21 @@ class EarningsServiceImplTest {
|
||||
verify(baseMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCounterLine_throwsWhenNoReferencePresent() {
|
||||
when(baseMapper.selectList(any())).thenReturn(Collections.emptyList());
|
||||
|
||||
assertThrows(IllegalStateException.class, () ->
|
||||
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.TEN, "admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCounterLine_persistsNegativeAvailableLine() {
|
||||
EarningsLineEntity reference = new EarningsLineEntity();
|
||||
reference.setAmount(new BigDecimal("88.00"));
|
||||
reference.setUnlockTime(LocalDateTime.now().minusHours(1));
|
||||
reference.setStatus("available");
|
||||
when(baseMapper.selectList(any())).thenReturn(Collections.singletonList(reference));
|
||||
when(baseMapper.insert(any())).thenReturn(1);
|
||||
|
||||
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.valueOf(88), "admin");
|
||||
@@ -97,6 +113,46 @@ class EarningsServiceImplTest {
|
||||
assertEquals("available", saved.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCounterLine_inheritsFrozenUnlockScheduleFromReference() {
|
||||
LocalDateTime unlockAt = LocalDateTime.now().plusDays(1).withNano(0);
|
||||
EarningsLineEntity reference = new EarningsLineEntity();
|
||||
reference.setAmount(new BigDecimal("150.00"));
|
||||
reference.setUnlockTime(unlockAt);
|
||||
reference.setStatus("frozen");
|
||||
when(baseMapper.selectList(any())).thenReturn(Collections.singletonList(reference));
|
||||
when(baseMapper.insert(any())).thenReturn(1);
|
||||
|
||||
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.valueOf(40), "admin");
|
||||
|
||||
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
|
||||
verify(baseMapper).insert(captor.capture());
|
||||
EarningsLineEntity saved = captor.getValue();
|
||||
assertEquals("frozen", saved.getStatus());
|
||||
assertEquals(unlockAt, saved.getUnlockTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCounterLine_unlockedReferenceProducesAvailableCounter() {
|
||||
LocalDateTime unlockAt = LocalDateTime.now().minusHours(3).withNano(0);
|
||||
EarningsLineEntity reference = new EarningsLineEntity();
|
||||
reference.setAmount(new BigDecimal("95.00"));
|
||||
reference.setUnlockTime(unlockAt);
|
||||
reference.setStatus("available");
|
||||
when(baseMapper.selectList(any())).thenReturn(Collections.singletonList(reference));
|
||||
when(baseMapper.insert(any())).thenReturn(1);
|
||||
|
||||
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.valueOf(55), "admin");
|
||||
|
||||
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
|
||||
verify(baseMapper).insert(captor.capture());
|
||||
EarningsLineEntity saved = captor.getValue();
|
||||
assertEquals("available", saved.getStatus());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
assertTrue(saved.getUnlockTime().isAfter(unlockAt));
|
||||
assertTrue(!saved.getUnlockTime().isAfter(now.plusSeconds(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findWithdrawable_returnsEmptyWhenCounterLinesCreateDebt() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -125,6 +181,34 @@ class EarningsServiceImplTest {
|
||||
assertEquals("second", picked.get(2).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findWithdrawable_handlesMixedPositiveAndNegativeSequences() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<EarningsLineEntity> lines = Arrays.asList(
|
||||
line("neg-30", new BigDecimal("-30")),
|
||||
line("pos-20", new BigDecimal("20")),
|
||||
line("pos-50", new BigDecimal("50")),
|
||||
line("neg-10", new BigDecimal("-10")),
|
||||
line("pos-40", new BigDecimal("40")),
|
||||
line("pos-60", new BigDecimal("60")),
|
||||
line("pos-15", new BigDecimal("15")),
|
||||
line("pos-25", new BigDecimal("25")),
|
||||
line("neg-5", new BigDecimal("-5")),
|
||||
line("pos-100", new BigDecimal("100")));
|
||||
|
||||
when(baseMapper.selectWithdrawableLines("clerk-mix", now)).thenReturn(lines, lines, lines);
|
||||
|
||||
List<EarningsLineEntity> partial = earningsService.findWithdrawable("clerk-mix", new BigDecimal("35"), now);
|
||||
assertEquals(Arrays.asList("neg-30", "pos-20", "pos-50"), ids(partial));
|
||||
|
||||
List<EarningsLineEntity> mid = earningsService.findWithdrawable("clerk-mix", new BigDecimal("90"), now);
|
||||
assertEquals(Arrays.asList("neg-30", "pos-20", "pos-50", "neg-10", "pos-40", "pos-60"), ids(mid));
|
||||
|
||||
List<EarningsLineEntity> full = earningsService.findWithdrawable("clerk-mix", new BigDecimal("265"), now);
|
||||
assertEquals(lines.size(), full.size());
|
||||
assertEquals("pos-100", full.get(full.size() - 1).getId());
|
||||
}
|
||||
|
||||
private EarningsLineEntity line(String id, BigDecimal amount) {
|
||||
EarningsLineEntity entity = new EarningsLineEntity();
|
||||
entity.setId(id);
|
||||
@@ -132,4 +216,8 @@ class EarningsServiceImplTest {
|
||||
entity.setStatus("available");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private List<String> ids(List<EarningsLineEntity> entities) {
|
||||
return entities.stream().map(EarningsLineEntity::getId).collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user