All checks were successful
Build and Deploy / deploy (push) Successful in 3m50s
Co-authored-by: Cursor <cursoragent@cursor.com>
271 lines
10 KiB
C#
271 lines
10 KiB
C#
using Fake.DependencyInjection;
|
||
using Fake.Application;
|
||
using Fake.Domain.Exceptions;
|
||
using Oms.Domain.CustomersAggregate.Repositories;
|
||
using Oms.Domain.MarketingAggregate.Entities;
|
||
using Oms.Domain.MarketingAggregate.Enums;
|
||
using Oms.Domain.MarketingAggregate.Repositories;
|
||
using Oms.Domain.OrdersAggregate.Consts;
|
||
using Oms.Domain.OrdersAggregate.Entities;
|
||
using Oms.Domain.OrdersAggregate.Repositories;
|
||
using System.Text.Json;
|
||
|
||
namespace Oms.Application.Shared;
|
||
|
||
/// <summary>
|
||
/// 引荐分成:派送成功按实付基础运费百分比计奖,T+N 工作日入运费余额。
|
||
/// </summary>
|
||
public class ReferralCommissionManager : IScopedDependency
|
||
{
|
||
private readonly IReferralCommissionRuleRepository _ruleRepository;
|
||
private readonly IReferralCommissionDetailRepository _detailRepository;
|
||
private readonly ICustomerRepository _customerRepository;
|
||
private readonly IMemberLevelRepository _memberLevelRepository;
|
||
private readonly IOrderRepository _orderRepository;
|
||
private readonly CustomerBalanceMutator _balanceMutator;
|
||
|
||
public ReferralCommissionManager(
|
||
IReferralCommissionRuleRepository ruleRepository,
|
||
IReferralCommissionDetailRepository detailRepository,
|
||
ICustomerRepository customerRepository,
|
||
IMemberLevelRepository memberLevelRepository,
|
||
IOrderRepository orderRepository,
|
||
CustomerBalanceMutator balanceMutator)
|
||
{
|
||
_ruleRepository = ruleRepository;
|
||
_detailRepository = detailRepository;
|
||
_customerRepository = customerRepository;
|
||
_memberLevelRepository = memberLevelRepository;
|
||
_orderRepository = orderRepository;
|
||
_balanceMutator = balanceMutator;
|
||
}
|
||
|
||
public async Task OnOrderDeliverySuccessAsync(int orderId, CancellationToken cancellationToken = default)
|
||
{
|
||
var order = await _orderRepository.FirstOrDefaultAsync(x => x.Id == orderId, cancellationToken)
|
||
?? throw new BusinessException("订单不存在");
|
||
if (!string.Equals(order.OrderStatus, OrderStatusCodes.DeliverySuccess, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new BusinessException("订单未达到派送成功状态");
|
||
}
|
||
|
||
if (order.IsRefunded > 0 || !order.CustomerId.HasValue)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var sessionCustomer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value, cancellationToken);
|
||
if (sessionCustomer == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var mainCustomer = await MarketingCustomerHelper.ResolveMarketingMainCustomerAsync(
|
||
_customerRepository, sessionCustomer, cancellationToken);
|
||
if (!mainCustomer.ReferrerCustomerId.HasValue || mainCustomer.ReferrerCustomerId.Value <= 0
|
||
|| mainCustomer.ReferrerCustomerId.Value == mainCustomer.Id)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var referrerId = mainCustomer.ReferrerCustomerId.Value;
|
||
var existing = await _detailRepository.GetByOrderAndReferrerAsync(orderId, referrerId, cancellationToken);
|
||
if (existing != null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var rule = await _ruleRepository.GetActiveRuleAsync(cancellationToken);
|
||
if (rule == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var referrer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == referrerId, cancellationToken);
|
||
if (referrer == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var referrerLevel = referrer.MemberLevelId.HasValue
|
||
? await _memberLevelRepository.FirstOrDefaultAsync(x => x.Id == referrer.MemberLevelId.Value, cancellationToken)
|
||
: null;
|
||
if (!MarketingInviteRewardCalculator.CanInvite(referrerLevel?.LevelCode, rule.MinInviteLevelCode))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var familyIds = await ResolveFamilyCustomerIdsAsync(mainCustomer.Id, cancellationToken);
|
||
var completedOrders = await _orderRepository.GetListAsync(
|
||
x => x.CustomerId != null
|
||
&& familyIds.Contains(x.CustomerId.Value)
|
||
&& x.OrderStatus == OrderStatusCodes.DeliverySuccess
|
||
&& x.IsRefunded == 0);
|
||
var ordered = completedOrders
|
||
.OrderBy(x => x.SignTime ?? x.PayTime ?? x.CreateTime)
|
||
.ThenBy(x => x.Id)
|
||
.ToList();
|
||
var orderIndex = ordered.FindIndex(x => x.Id == order.Id) + 1;
|
||
if (orderIndex <= 0)
|
||
{
|
||
orderIndex = ordered.Count + 1;
|
||
}
|
||
|
||
var fees = await _orderRepository.GetFeesByOrderIdsAsync([order.Id]);
|
||
var paidBaseFreight = MarketingFeeHelper.ResolvePaidBaseFreight(
|
||
MarketingFeeHelper.SumReceivableBaseFreight(fees),
|
||
order.LevelDiscountAmount,
|
||
order.CouponDiscountAmount);
|
||
|
||
var snapshot = ToRewardSnapshot(rule);
|
||
var completeTime = order.SignTime ?? DateTime.Now;
|
||
var decision = MarketingInviteRewardCalculator.Resolve(
|
||
paidBaseFreight,
|
||
orderIndex,
|
||
mainCustomer.CreateTime,
|
||
completeTime,
|
||
snapshot);
|
||
if (!decision.Eligible || decision.Amount <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var settleDue = MarketingInviteRewardCalculator.AddBusinessDays(completeTime, snapshot.SettleBusinessDays);
|
||
await _detailRepository.InsertAsync(new ReferralCommissionDetail
|
||
{
|
||
ReferrerCustomerId = referrerId,
|
||
RefereeCustomerId = mainCustomer.Id,
|
||
OrderId = order.Id,
|
||
OrderNo = order.ServiceNo,
|
||
BasePayableAmount = paidBaseFreight,
|
||
CommissionAmount = decision.Amount,
|
||
Status = CommissionStatus.Pending,
|
||
EligibleTime = completeTime,
|
||
SettleDueTime = settleDue,
|
||
RewardOrderIndex = decision.OrderIndex,
|
||
RuleSnapshotJson = JsonSerializer.Serialize(snapshot)
|
||
}, cancellationToken);
|
||
}
|
||
|
||
public async Task SettlePendingAsync(int batchSize, CancellationToken cancellationToken = default)
|
||
{
|
||
var pendingList = await _detailRepository.GetPendingSettleAsync(DateTime.Now, batchSize, cancellationToken);
|
||
foreach (var detail in pendingList)
|
||
{
|
||
if (!IsDue(detail, DateTime.Now))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var referrer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == detail.ReferrerCustomerId, cancellationToken);
|
||
if (referrer == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
await _balanceMutator.CreditReferralRewardAsync(
|
||
referrer,
|
||
detail.CommissionAmount,
|
||
detail.Id,
|
||
detail.OrderNo,
|
||
$"邀请奖励 {detail.OrderNo}",
|
||
cancellationToken);
|
||
|
||
detail.Status = CommissionStatus.Settled;
|
||
detail.SettleTime = DateTime.Now;
|
||
await _detailRepository.UpdateAsync(detail, cancellationToken);
|
||
}
|
||
}
|
||
|
||
public async Task ClawbackOnOrderRefundAsync(Order order, CancellationToken cancellationToken = default)
|
||
{
|
||
var detail = await _detailRepository.GetByOrderIdAsync(order.Id, cancellationToken);
|
||
if (detail == null || detail.Status == CommissionStatus.Cancelled)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (detail.Status == CommissionStatus.Pending)
|
||
{
|
||
detail.Status = CommissionStatus.Cancelled;
|
||
detail.CancelReason = "订单退款扣回";
|
||
await _detailRepository.UpdateAsync(detail, cancellationToken);
|
||
return;
|
||
}
|
||
|
||
if (detail.Status == CommissionStatus.Settled)
|
||
{
|
||
var referrer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == detail.ReferrerCustomerId, cancellationToken);
|
||
if (referrer != null)
|
||
{
|
||
await _balanceMutator.DebitReferralClawbackAsync(
|
||
referrer,
|
||
detail.CommissionAmount,
|
||
detail.Id,
|
||
detail.OrderNo,
|
||
$"邀请奖励退款扣回 {detail.OrderNo}",
|
||
cancellationToken);
|
||
}
|
||
|
||
detail.Status = CommissionStatus.Cancelled;
|
||
detail.CancelReason = "订单退款扣回";
|
||
await _detailRepository.UpdateAsync(detail, cancellationToken);
|
||
}
|
||
}
|
||
|
||
private async Task<int[]> ResolveFamilyCustomerIdsAsync(int mainCustomerId, CancellationToken cancellationToken)
|
||
{
|
||
var children = await _customerRepository.GetListAsync(x => x.ParentCustomerId == mainCustomerId);
|
||
return children.Select(x => x.Id).Append(mainCustomerId).ToArray();
|
||
}
|
||
|
||
private static bool IsDue(ReferralCommissionDetail detail, DateTime now)
|
||
{
|
||
if (detail.SettleDueTime.HasValue)
|
||
{
|
||
return detail.SettleDueTime.Value <= now;
|
||
}
|
||
|
||
if (detail.EligibleTime.HasValue)
|
||
{
|
||
var freezeDays = 7;
|
||
try
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(detail.RuleSnapshotJson))
|
||
{
|
||
using var doc = JsonDocument.Parse(detail.RuleSnapshotJson);
|
||
if (doc.RootElement.TryGetProperty("SettleBusinessDays", out var days))
|
||
{
|
||
freezeDays = days.GetInt32();
|
||
}
|
||
else if (doc.RootElement.TryGetProperty("FreezeDays", out var oldDays))
|
||
{
|
||
freezeDays = oldDays.GetInt32();
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// ignore
|
||
}
|
||
|
||
return MarketingInviteRewardCalculator.AddBusinessDays(detail.EligibleTime.Value, freezeDays) <= now;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private static InviteRewardRuleSnapshot ToRewardSnapshot(ReferralCommissionRule rule) => new()
|
||
{
|
||
MinPaidBaseFreight = rule.MinPaidBaseFreight > 0 ? rule.MinPaidBaseFreight : 50M,
|
||
FirstOrderRate = rule.FirstOrderRate > 0 ? rule.FirstOrderRate : 0.05M,
|
||
FirstOrderCap = rule.FirstOrderCap > 0 ? rule.FirstOrderCap : 50M,
|
||
FollowOrderRate = rule.FollowOrderRate > 0 ? rule.FollowOrderRate : 0.03M,
|
||
FollowOrderCap = rule.FollowOrderCap > 0 ? rule.FollowOrderCap : 30M,
|
||
FollowWindowDays = rule.FollowWindowDays > 0 ? rule.FollowWindowDays : 90,
|
||
MaxRewardOrders = rule.MaxRewardOrders > 0 ? rule.MaxRewardOrders : 6,
|
||
SettleBusinessDays = rule.SettleBusinessDays > 0 ? rule.SettleBusinessDays : rule.FreezeDays,
|
||
MinInviteLevelCode = string.IsNullOrWhiteSpace(rule.MinInviteLevelCode) ? "V1" : rule.MinInviteLevelCode
|
||
};
|
||
}
|