WIP
This commit is contained in:
@@ -7,6 +7,7 @@ import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
|
||||
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.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.module.vo.*;
|
||||
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
||||
@@ -106,6 +107,26 @@ public class PlayOrderInfoController {
|
||||
return R.ok("退款成功");
|
||||
}
|
||||
|
||||
@ApiOperation(value = "撤销已完成订单", notes = "管理员操作撤销,支持可选退款与收益处理")
|
||||
@PostMapping("/revokeCompleted")
|
||||
public R revokeCompleted(@Validated @RequestBody PlayOrderRevocationVo vo) {
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId(vo.getOrderId());
|
||||
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.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setOperatorId(SecurityUtils.getUserId());
|
||||
context.withTriggerSource(OrderTriggerSource.ADMIN_API);
|
||||
orderLifecycleService.revokeCompletedOrder(context);
|
||||
return R.ok("撤销成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理后台强制取消进行中订单
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.starry.admin.modules.order.listener;
|
||||
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
@Component
|
||||
public class OrderRevocationBalanceListener {
|
||||
|
||||
private final IPlayCustomUserInfoService customUserInfoService;
|
||||
|
||||
public OrderRevocationBalanceListener(IPlayCustomUserInfoService customUserInfoService) {
|
||||
this.customUserInfoService = customUserInfoService;
|
||||
}
|
||||
|
||||
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
|
||||
public void handle(OrderRevocationEvent event) {
|
||||
if (event == null || event.getContext() == null || event.getOrderSnapshot() == null) {
|
||||
return;
|
||||
}
|
||||
if (!event.getContext().isRefundToCustomer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
BigDecimal refundAmount = Optional.ofNullable(event.getContext().getRefundAmount()).orElse(BigDecimal.ZERO);
|
||||
if (refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayOrderInfoEntity order = event.getOrderSnapshot();
|
||||
PlayCustomUserInfoEntity customer = customUserInfoService.getById(order.getPurchaserBy());
|
||||
if (customer == null) {
|
||||
throw new CustomException("顾客信息不存在");
|
||||
}
|
||||
BigDecimal currentBalance = Optional.ofNullable(customer.getAccountBalance()).orElse(BigDecimal.ZERO);
|
||||
customUserInfoService.updateAccountBalanceById(
|
||||
customer.getId(),
|
||||
currentBalance,
|
||||
currentBalance.add(refundAmount),
|
||||
BalanceOperationType.REFUND.getCode(),
|
||||
"已完成订单撤销退款",
|
||||
refundAmount,
|
||||
BigDecimal.ZERO,
|
||||
order.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.starry.admin.modules.order.listener;
|
||||
|
||||
import com.starry.admin.common.exception.CustomException;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
@Component
|
||||
public class OrderRevocationEarningsListener {
|
||||
|
||||
private final IEarningsService earningsService;
|
||||
|
||||
public OrderRevocationEarningsListener(IEarningsService earningsService) {
|
||||
this.earningsService = earningsService;
|
||||
}
|
||||
|
||||
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
|
||||
public void handle(OrderRevocationEvent event) {
|
||||
if (event == null || event.getContext() == null || event.getOrderSnapshot() == null) {
|
||||
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("未知的收益处理策略");
|
||||
}
|
||||
}
|
||||
|
||||
private void createCounterLine(OrderRevocationEvent event) {
|
||||
OrderRevocationContext context = event.getContext();
|
||||
PlayOrderInfoEntity order = event.getOrderSnapshot();
|
||||
String targetClerkId = context.getCounterClerkId();
|
||||
if (targetClerkId == null) {
|
||||
throw new CustomException("需要指定收益冲销目标账号");
|
||||
}
|
||||
BigDecimal amount = context.getRefundAmount();
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
amount = Optional.ofNullable(order.getEstimatedRevenue()).orElse(BigDecimal.ZERO);
|
||||
}
|
||||
earningsService.createCounterLine(order.getId(), order.getTenantId(), targetClerkId, amount, context.getOperatorId());
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,8 @@ public class OrderConstant {
|
||||
ACCEPTED("1", "已接单(待开始)"),
|
||||
IN_PROGRESS("2", "已开始(服务中)"),
|
||||
COMPLETED("3", "已完成"),
|
||||
CANCELLED("4", "已取消");
|
||||
CANCELLED("4", "已取消"),
|
||||
REVOKED("5", "已撤销");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.starry.admin.modules.order.module.dto;
|
||||
|
||||
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
|
||||
import java.math.BigDecimal;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@Data
|
||||
public class OrderRevocationContext {
|
||||
|
||||
@NotBlank
|
||||
private String orderId;
|
||||
|
||||
@Nullable
|
||||
private String operatorId;
|
||||
|
||||
@Nullable
|
||||
private String operatorType;
|
||||
|
||||
@Nullable
|
||||
private BigDecimal refundAmount;
|
||||
|
||||
@Nullable
|
||||
private String refundReason;
|
||||
|
||||
private boolean refundToCustomer;
|
||||
|
||||
private EarningsAdjustStrategy earningsStrategy = EarningsAdjustStrategy.NONE;
|
||||
|
||||
@Nullable
|
||||
private String counterClerkId;
|
||||
|
||||
private OrderTriggerSource triggerSource = OrderTriggerSource.UNKNOWN;
|
||||
|
||||
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,17 @@
|
||||
package com.starry.admin.modules.order.module.event;
|
||||
|
||||
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class OrderRevocationEvent {
|
||||
|
||||
private final OrderRevocationContext context;
|
||||
private final PlayOrderInfoEntity orderSnapshot;
|
||||
|
||||
public OrderRevocationEvent(OrderRevocationContext context, PlayOrderInfoEntity orderSnapshot) {
|
||||
this.context = context;
|
||||
this.orderSnapshot = orderSnapshot;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@ApiModel(value = "订单撤销参数", description = "撤销已完成订单的请求参数")
|
||||
public class PlayOrderRevocationVo {
|
||||
|
||||
@NotBlank(message = "订单ID不能为空")
|
||||
@ApiModelProperty(value = "订单ID", required = true)
|
||||
private String orderId;
|
||||
|
||||
@ApiModelProperty(value = "是否退还顾客余额")
|
||||
private boolean refundToCustomer;
|
||||
|
||||
@ApiModelProperty(value = "退款金额,未填写则默认订单实付金额")
|
||||
private BigDecimal refundAmount;
|
||||
|
||||
@ApiModelProperty(value = "撤销原因")
|
||||
private String refundReason;
|
||||
|
||||
@ApiModelProperty(value = "收益处理策略:NONE/REVERSE_CLERK/COUNTER_TO_PEIPEI")
|
||||
private EarningsAdjustStrategy earningsStrategy = EarningsAdjustStrategy.NONE;
|
||||
|
||||
@ApiModelProperty(value = "收益冲销目标账号ID,策略为 COUNTER_TO_PEIPEI 时必填")
|
||||
private String counterClerkId;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
|
||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
|
||||
public interface IOrderLifecycleService {
|
||||
@@ -14,4 +15,6 @@ public interface IOrderLifecycleService {
|
||||
void completeOrder(String orderId, OrderCompletionContext context);
|
||||
|
||||
void refundOrder(OrderRefundContext context);
|
||||
|
||||
void revokeCompletedOrder(OrderRevocationContext context);
|
||||
}
|
||||
|
||||
@@ -199,6 +199,8 @@ public interface IPlayOrderInfoService extends IService<PlayOrderInfoEntity> {
|
||||
*/
|
||||
List<PlayOrderInfoEntity> customSelectOrderInfoByList(String customId);
|
||||
|
||||
void revokeCompletedOrder(OrderRevocationContext context);
|
||||
|
||||
/**
|
||||
* 修改订单状态为接单 只有管理员或者店员本人才能操作
|
||||
*
|
||||
|
||||
@@ -34,10 +34,12 @@ import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
|
||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderLogInfoEntity;
|
||||
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
||||
import com.starry.admin.modules.order.service.IOrderLifecycleService;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||
@@ -61,9 +63,11 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -77,7 +81,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
private enum LifecycleOperation {
|
||||
CREATE,
|
||||
COMPLETE,
|
||||
REFUND
|
||||
REFUND,
|
||||
REVOKE_COMPLETED
|
||||
}
|
||||
|
||||
@Resource
|
||||
@@ -110,6 +115,9 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
@Resource
|
||||
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
||||
|
||||
@Resource
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
private Map<StrategyKey, OrderPlacementStrategy> placementStrategies;
|
||||
|
||||
@PostConstruct
|
||||
@@ -520,7 +528,8 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
throw new CustomException("每个订单只能退款一次~");
|
||||
}
|
||||
|
||||
if (isBalancePaidOrder(order) && !playBalanceDetailsInfoService.existsCustomerConsumeRecord(order.getPurchaserBy(), order.getId())) {
|
||||
if (isBalancePaidOrder(order)
|
||||
&& !playBalanceDetailsInfoService.existsCustomerConsumeRecord(order.getPurchaserBy(), order.getId())) {
|
||||
throw new CustomException("订单未发生余额扣款,无法退款");
|
||||
}
|
||||
|
||||
@@ -603,6 +612,107 @@ public class OrderLifecycleServiceImpl implements IOrderLifecycleService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void revokeCompletedOrder(OrderRevocationContext context) {
|
||||
if (context == null || StrUtil.isBlank(context.getOrderId())) {
|
||||
throw new CustomException("订单ID不能为空");
|
||||
}
|
||||
PlayOrderInfoEntity order = orderInfoMapper.selectById(context.getOrderId());
|
||||
if (order == null) {
|
||||
throw new CustomException("订单不存在");
|
||||
}
|
||||
if (OrderStatus.REVOKED.getCode().equals(order.getOrderStatus())) {
|
||||
throw new CustomException("订单已撤销");
|
||||
}
|
||||
if (!OrderStatus.COMPLETED.getCode().equals(order.getOrderStatus())) {
|
||||
throw new CustomException("当前状态无法撤销");
|
||||
}
|
||||
|
||||
OrderRevocationContext.EarningsAdjustStrategy strategy = context.getEarningsStrategy() != null
|
||||
? context.getEarningsStrategy()
|
||||
: OrderRevocationContext.EarningsAdjustStrategy.NONE;
|
||||
if (strategy != OrderRevocationContext.EarningsAdjustStrategy.NONE && 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);
|
||||
|
||||
BigDecimal finalAmount = Optional.ofNullable(order.getFinalAmount()).orElse(BigDecimal.ZERO);
|
||||
BigDecimal refundAmount = context.getRefundAmount();
|
||||
if (refundAmount == null) {
|
||||
refundAmount = context.isRefundToCustomer() ? finalAmount : BigDecimal.ZERO;
|
||||
}
|
||||
if (refundAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new CustomException("退款金额不能小于0");
|
||||
}
|
||||
if (refundAmount.compareTo(finalAmount) > 0) {
|
||||
throw new CustomException("退款金额不能大于支付金额");
|
||||
}
|
||||
context.setRefundAmount(refundAmount);
|
||||
|
||||
if (refundAmount.compareTo(BigDecimal.ZERO) > 0 && context.isRefundToCustomer()) {
|
||||
if (isBalancePaidOrder(order)
|
||||
&& !playBalanceDetailsInfoService.existsCustomerConsumeRecord(order.getPurchaserBy(), order.getId())) {
|
||||
throw new CustomException("订单未发生余额扣款,无法退款");
|
||||
}
|
||||
}
|
||||
|
||||
UpdateWrapper<PlayOrderInfoEntity> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", order.getId())
|
||||
.eq("order_status", OrderStatus.COMPLETED.getCode())
|
||||
.set("order_status", OrderStatus.REVOKED.getCode())
|
||||
.set("order_cancel_time", LocalDateTime.now())
|
||||
.set("refund_amount", refundAmount)
|
||||
.set("refund_reason", context.getRefundReason());
|
||||
if (refundAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
updateWrapper.set("refund_type", OrderRefundFlag.REFUNDED.getCode());
|
||||
}
|
||||
|
||||
boolean updated = orderInfoMapper.update(null, updateWrapper) > 0;
|
||||
PlayOrderInfoEntity latest = orderInfoMapper.selectById(order.getId());
|
||||
if (!updated && (latest == null || !OrderStatus.REVOKED.getCode().equals(latest.getOrderStatus()))) {
|
||||
throw new CustomException("订单状态已变化,无法撤销");
|
||||
}
|
||||
if (latest == null) {
|
||||
latest = order;
|
||||
latest.setOrderStatus(OrderStatus.REVOKED.getCode());
|
||||
}
|
||||
|
||||
if (refundAmount.compareTo(BigDecimal.ZERO) > 0 && context.isRefundToCustomer()) {
|
||||
OrderRefundRecordType recordType = finalAmount.compareTo(refundAmount) == 0
|
||||
? OrderRefundRecordType.FULL
|
||||
: OrderRefundRecordType.PARTIAL;
|
||||
orderRefundInfoService.add(
|
||||
latest.getId(),
|
||||
latest.getPurchaserBy(),
|
||||
latest.getAcceptBy(),
|
||||
latest.getPayMethod(),
|
||||
recordType.getCode(),
|
||||
refundAmount,
|
||||
context.getRefundReason(),
|
||||
context.getOperatorType(),
|
||||
context.getOperatorId(),
|
||||
OrderRefundState.PROCESSING.getCode(),
|
||||
ReviewRequirement.NOT_REQUIRED.getCode());
|
||||
}
|
||||
|
||||
OrderActor actor = resolveCompletionActor(context.getOperatorType());
|
||||
String operationType = String.format(
|
||||
"%s_%s",
|
||||
LifecycleOperation.REVOKE_COMPLETED.name(),
|
||||
strategy != null
|
||||
? strategy.getLogCode()
|
||||
: OrderRevocationContext.EarningsAdjustStrategy.NONE.getLogCode());
|
||||
recordOrderLog(latest, actor, context.getOperatorId(), LifecycleOperation.REVOKE_COMPLETED,
|
||||
context.getRefundReason(), operationType);
|
||||
|
||||
applicationEventPublisher.publishEvent(new OrderRevocationEvent(context, latest));
|
||||
}
|
||||
|
||||
private void validateOrderCreationRequest(OrderCreationContext context) {
|
||||
if (context == null) {
|
||||
throw new CustomException("订单创建请求不能为空");
|
||||
|
||||
@@ -943,6 +943,14 @@ public class PlayOrderInfoServiceImpl extends ServiceImpl<PlayOrderInfoMapper, P
|
||||
notificationSender.sendOrderCancelMessageAsync(latest, refundReason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revokeCompletedOrder(OrderRevocationContext context) {
|
||||
if (context == null || StrUtil.isBlank(context.getOrderId())) {
|
||||
throw new CustomException("订单信息缺失");
|
||||
}
|
||||
orderLifecycleService.revokeCompletedOrder(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayOrderInfoEntity queryByOrderNo(String orderNo) {
|
||||
LambdaQueryWrapper<PlayOrderInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
@@ -17,4 +17,10 @@ public interface IEarningsService extends IService<EarningsLineEntity> {
|
||||
LocalDateTime getNextUnlockTime(String clerkId, LocalDateTime now);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||
@@ -12,6 +14,7 @@ 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;
|
||||
@@ -90,4 +93,49 @@ public class EarningsServiceImpl extends ServiceImpl<EarningsLineMapper, Earning
|
||||
if (acc.compareTo(amount) < 0) return new ArrayList<>();
|
||||
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)) {
|
||||
throw new IllegalArgumentException("创建冲销收益时参数缺失");
|
||||
}
|
||||
BigDecimal normalized = amount == null ? BigDecimal.ZERO : amount.abs();
|
||||
if (normalized.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
this.save(line);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLockedLines(String orderId) {
|
||||
if (StrUtil.isBlank(orderId)) {
|
||||
return false;
|
||||
}
|
||||
Long count = this.lambdaQuery()
|
||||
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||
.in(EarningsLineEntity::getStatus, Arrays.asList("withdrawing", "withdrawn"))
|
||||
.count();
|
||||
return count != null && count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.assertj.core.api.SoftAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
class WxBlindBoxOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.starry.admin.modules.order.listener;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
|
||||
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRevocationContext.EarningsAdjustStrategy;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||
import java.math.BigDecimal;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OrderRevocationEarningsListenerTest {
|
||||
|
||||
@Mock
|
||||
private IEarningsService earningsService;
|
||||
|
||||
private OrderRevocationEarningsListener listener;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
listener = new OrderRevocationEarningsListener(earningsService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handle_counterStrategyFallsBackToEstimatedRevenueWhenRefundAmountZero() {
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId("order-counter-1");
|
||||
context.setOperatorId("admin-op");
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.COUNTER_TO_PEIPEI);
|
||||
context.setRefundAmount(BigDecimal.ZERO);
|
||||
context.setCounterClerkId("ops-clerk");
|
||||
|
||||
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||
order.setId("order-counter-1");
|
||||
order.setTenantId("tenant-77");
|
||||
order.setEstimatedRevenue(BigDecimal.valueOf(68));
|
||||
|
||||
listener.handle(new OrderRevocationEvent(context, order));
|
||||
|
||||
verify(earningsService)
|
||||
.createCounterLine(order.getId(), order.getTenantId(), "ops-clerk", BigDecimal.valueOf(68), "admin-op");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handle_reverseStrategyRevertsAvailableLines() {
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId("order-reverse-2");
|
||||
context.setOperatorId("admin-reviewer");
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
|
||||
|
||||
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||
order.setId("order-reverse-2");
|
||||
|
||||
listener.handle(new OrderRevocationEvent(context, order));
|
||||
|
||||
verify(earningsService).reverseByOrder("order-reverse-2", "admin-reviewer");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handle_noneStrategyIsNoOp() {
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId("order-none-3");
|
||||
context.setOperatorId("admin-noop");
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.NONE);
|
||||
|
||||
PlayOrderInfoEntity order = new PlayOrderInfoEntity();
|
||||
order.setId("order-none-3");
|
||||
|
||||
listener.handle(new OrderRevocationEvent(context, order));
|
||||
|
||||
verifyNoInteractions(earningsService);
|
||||
}
|
||||
}
|
||||
@@ -40,9 +40,12 @@ import com.starry.admin.modules.order.module.dto.OrderCreationContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderPlacementCommand;
|
||||
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRevocationContext;
|
||||
import com.starry.admin.modules.order.module.dto.OrderRevocationContext.EarningsAdjustStrategy;
|
||||
import com.starry.admin.modules.order.module.dto.PaymentInfo;
|
||||
import com.starry.admin.modules.order.module.dto.RandomOrderRequirements;
|
||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||
import com.starry.admin.modules.order.module.event.OrderRevocationEvent;
|
||||
import com.starry.admin.modules.order.module.vo.ClerkEstimatedRevenueVo;
|
||||
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
|
||||
import com.starry.admin.modules.order.service.support.ClerkRevenueCalculator;
|
||||
@@ -65,10 +68,12 @@ import java.util.UUID;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class OrderLifecycleServiceImplTest {
|
||||
@@ -106,11 +111,104 @@ class OrderLifecycleServiceImplTest {
|
||||
@Mock
|
||||
private IPlayBalanceDetailsInfoService playBalanceDetailsInfoService;
|
||||
|
||||
@Mock
|
||||
private ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
@BeforeEach
|
||||
void initStrategies() {
|
||||
lifecycleService.initPlacementStrategies();
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_updatesStatusAndPublishesEvent() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||
completed.setFinalAmount(BigDecimal.valueOf(188));
|
||||
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||
revoked.setFinalAmount(BigDecimal.valueOf(188));
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||
when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||
when(earningsService.hasLockedLines(orderId)).thenReturn(false);
|
||||
PlayCustomUserInfoEntity customer = buildCustomer(completed.getPurchaserBy(), BigDecimal.ZERO);
|
||||
when(customUserInfoService.getById(completed.getPurchaserBy())).thenReturn(customer);
|
||||
doNothing().when(customUserInfoService)
|
||||
.updateAccountBalanceById(anyString(), any(), any(), anyString(), anyString(), any(), any(), anyString());
|
||||
doNothing().when(orderRefundInfoService)
|
||||
.add(anyString(), anyString(), anyString(), anyString(), anyString(), any(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId(orderId);
|
||||
context.setOperatorId("admin-8");
|
||||
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setRefundAmount(BigDecimal.valueOf(88));
|
||||
context.setRefundReason("客户投诉");
|
||||
context.setRefundToCustomer(true);
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
|
||||
|
||||
lifecycleService.revokeCompletedOrder(context);
|
||||
|
||||
verify(orderInfoMapper).update(isNull(), any());
|
||||
verify(orderLogInfoMapper).insert(any());
|
||||
ArgumentCaptor<OrderRevocationEvent> captor = ArgumentCaptor.forClass(OrderRevocationEvent.class);
|
||||
verify(applicationEventPublisher).publishEvent(captor.capture());
|
||||
OrderRevocationEvent event = captor.getValue();
|
||||
assertEquals(orderId, event.getContext().getOrderId());
|
||||
assertEquals(EarningsAdjustStrategy.REVERSE_CLERK, event.getContext().getEarningsStrategy());
|
||||
assertEquals(BigDecimal.valueOf(88), event.getContext().getRefundAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_defersBalanceCreditToListener() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||
completed.setFinalAmount(BigDecimal.valueOf(208));
|
||||
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
PlayOrderInfoEntity revoked = buildOrder(orderId, OrderStatus.REVOKED.getCode());
|
||||
revoked.setFinalAmount(BigDecimal.valueOf(208));
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(completed, revoked);
|
||||
when(orderInfoMapper.update(isNull(), any())).thenReturn(1);
|
||||
when(earningsService.hasLockedLines(orderId)).thenReturn(false);
|
||||
when(customUserInfoService.getById(anyString())).thenThrow(new AssertionError("Balance update should be handled by listener"));
|
||||
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId(orderId);
|
||||
context.setOperatorId("admin-9");
|
||||
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setRefundAmount(BigDecimal.valueOf(108));
|
||||
context.setRefundReason("质量问题");
|
||||
context.setRefundToCustomer(true);
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.NONE);
|
||||
|
||||
lifecycleService.revokeCompletedOrder(context);
|
||||
|
||||
verify(customUserInfoService, never()).getById(anyString());
|
||||
verify(applicationEventPublisher).publishEvent(any(OrderRevocationEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokeCompletedOrder_blocksWhenEarningsLocked() {
|
||||
String orderId = UUID.randomUUID().toString();
|
||||
PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
|
||||
completed.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
|
||||
|
||||
when(orderInfoMapper.selectById(orderId)).thenReturn(completed);
|
||||
when(earningsService.hasLockedLines(orderId)).thenReturn(true);
|
||||
|
||||
OrderRevocationContext context = new OrderRevocationContext();
|
||||
context.setOrderId(orderId);
|
||||
context.setOperatorId("admin-locked");
|
||||
context.setOperatorType(OperatorType.ADMIN.getCode());
|
||||
context.setRefundToCustomer(false);
|
||||
context.setEarningsStrategy(EarningsAdjustStrategy.REVERSE_CLERK);
|
||||
|
||||
assertThrows(CustomException.class, () -> lifecycleService.revokeCompletedOrder(context));
|
||||
verify(orderInfoMapper, never()).update(isNull(), any());
|
||||
verify(applicationEventPublisher, never()).publishEvent(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void placeOrder_throwsWhenCommandNull() {
|
||||
assertThrows(CustomException.class, () -> lifecycleService.placeOrder(null));
|
||||
@@ -1223,6 +1321,13 @@ class OrderLifecycleServiceImplTest {
|
||||
return entity;
|
||||
}
|
||||
|
||||
private PlayCustomUserInfoEntity buildCustomer(String id, BigDecimal balance) {
|
||||
PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity();
|
||||
entity.setId(id);
|
||||
entity.setAccountBalance(balance);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void stubDefaultPersistence() {
|
||||
lenient().when(orderInfoMapper.selectCount(any())).thenReturn(0L);
|
||||
lenient().when(orderInfoMapper.insert(any())).thenReturn(1);
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
|
||||
import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -68,4 +70,54 @@ class EarningsServiceImplTest {
|
||||
|
||||
verify(baseMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCounterLine_persistsNegativeAvailableLine() {
|
||||
when(baseMapper.insert(any())).thenReturn(1);
|
||||
|
||||
earningsService.createCounterLine("order-neg", "tenant-t", "clerk-c", BigDecimal.valueOf(88), "admin");
|
||||
|
||||
ArgumentCaptor<EarningsLineEntity> captor = ArgumentCaptor.forClass(EarningsLineEntity.class);
|
||||
verify(baseMapper).insert(captor.capture());
|
||||
EarningsLineEntity saved = captor.getValue();
|
||||
assertEquals(new BigDecimal("-88"), saved.getAmount());
|
||||
assertEquals("clerk-c", saved.getClerkId());
|
||||
assertEquals("available", saved.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findWithdrawable_returnsEmptyWhenCounterLinesCreateDebt() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<EarningsLineEntity> lines = Arrays.asList(
|
||||
line("neg", new BigDecimal("-60")),
|
||||
line("pos", new BigDecimal("40")));
|
||||
when(baseMapper.selectWithdrawableLines("clerk-001", now)).thenReturn(lines);
|
||||
|
||||
List<EarningsLineEntity> picked = earningsService.findWithdrawable("clerk-001", BigDecimal.valueOf(30), now);
|
||||
|
||||
assertEquals(0, picked.size(), "净额不足时不应允许提现");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findWithdrawable_allowsWithdrawalAfterPositiveLinesCoverDebt() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<EarningsLineEntity> lines = Arrays.asList(
|
||||
line("neg", new BigDecimal("-60")),
|
||||
line("first", new BigDecimal("40")),
|
||||
line("second", new BigDecimal("150")));
|
||||
when(baseMapper.selectWithdrawableLines("clerk-001", now)).thenReturn(lines);
|
||||
|
||||
List<EarningsLineEntity> picked = earningsService.findWithdrawable("clerk-001", BigDecimal.valueOf(70), now);
|
||||
|
||||
assertEquals(3, picked.size());
|
||||
assertEquals("second", picked.get(2).getId());
|
||||
}
|
||||
|
||||
private EarningsLineEntity line(String id, BigDecimal amount) {
|
||||
EarningsLineEntity entity = new EarningsLineEntity();
|
||||
entity.setId(id);
|
||||
entity.setAmount(amount);
|
||||
entity.setStatus("available");
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user