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;
///
/// 引荐分成:派送成功按实付基础运费百分比计奖,T+N 工作日入运费余额。
///
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 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
};
}