Add Chinese XML summaries for OMS-Marketing endpoints, and mark OrderService batch worker methods internal to stop OMS-Order swagger generation conflicts. Co-authored-by: Cursor <cursoragent@cursor.com>
333 lines
13 KiB
C#
333 lines
13 KiB
C#
using System.Linq.Expressions;
|
|
using System.Text.Json;
|
|
using Fake.Application;
|
|
using Fake.Domain.Exceptions;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Oms.Application.Dtos.Marketing;
|
|
using Oms.Application.Dtos.Shared;
|
|
using Oms.Application.Shared;
|
|
using Oms.Domain.CustomersAggregate.Entities;
|
|
using Oms.Domain.CustomersAggregate.Repositories;
|
|
using Oms.Domain.MarketingAggregate.Consts;
|
|
using Oms.Domain.MarketingAggregate.Entities;
|
|
using Oms.Domain.MarketingAggregate.Enums;
|
|
using Oms.Domain.MarketingAggregate.Repositories;
|
|
|
|
namespace Oms.Application.Services.Marketing;
|
|
|
|
/// <summary>
|
|
/// ????????? API??????
|
|
/// </summary>
|
|
[ApiExplorerSettings(GroupName = "OMS-Marketing")]
|
|
public class MarketingCommissionAdminService : ApplicationService
|
|
{
|
|
private IReferralCommissionDetailRepository DetailRepository =>
|
|
LazyServiceProvider.GetRequiredService<IReferralCommissionDetailRepository>();
|
|
|
|
private ICommissionWithdrawRequestRepository WithdrawRepository =>
|
|
LazyServiceProvider.GetRequiredService<ICommissionWithdrawRequestRepository>();
|
|
|
|
private ICustomerRepository CustomerRepository =>
|
|
LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
|
|
|
private IMarketingAuditLogRepository AuditLogRepository =>
|
|
LazyServiceProvider.GetRequiredService<IMarketingAuditLogRepository>();
|
|
|
|
private CommissionBalanceMutator CommissionBalanceMutator =>
|
|
LazyServiceProvider.GetRequiredService<CommissionBalanceMutator>();
|
|
|
|
/// <summary>
|
|
/// ?????????
|
|
/// </summary>
|
|
[HttpPost("/marketing/commission-detail/list")]
|
|
public async Task<OmsPageResult<CommissionDetailOutput>> GetDetailListAsync(CommissionDetailListInput input)
|
|
{
|
|
var predicate = BuildDetailPredicate(input);
|
|
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
|
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
|
var entities = await DetailRepository.GetPagedListAsync(
|
|
predicate,
|
|
pageIndex,
|
|
pageSize,
|
|
new Dictionary<string, bool> { [nameof(ReferralCommissionDetail.CreateTime)] = false, [nameof(ReferralCommissionDetail.Id)] = false });
|
|
var total = await DetailRepository.CountAsync(predicate);
|
|
return new OmsPageResult<CommissionDetailOutput>
|
|
{
|
|
Items = await MapDetailOutputsAsync(entities),
|
|
Total = total
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// ?????????
|
|
/// </summary>
|
|
[HttpPost("/marketing/commission-detail/adjust")]
|
|
public async Task AdjustAsync(CommissionAdjustInput input)
|
|
{
|
|
if (input.Amount == 0)
|
|
{
|
|
throw new BusinessException("Adjust amount cannot be zero");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Remark) || input.Remark.Trim().Length < 10)
|
|
{
|
|
throw new BusinessException("Remark must be at least 10 characters");
|
|
}
|
|
|
|
var customer = await ResolveCustomerAsync(input.CustomerId, input.CustomerCode);
|
|
if (customer.ParentCustomerId.HasValue)
|
|
{
|
|
throw new BusinessException("Adjust main customer only");
|
|
}
|
|
|
|
var before = customer.CommissionBalance;
|
|
await CommissionBalanceMutator.AdjustCommissionAsync(customer, input.Amount, input.DetailId, input.Remark.Trim());
|
|
await AuditLogRepository.InsertAsync(new MarketingAuditLog
|
|
{
|
|
Action = MarketingAuditActions.AdjustCommission,
|
|
OperatorId = CurrentUser.GetId<int>(),
|
|
TargetType = "customer",
|
|
TargetId = customer.Id,
|
|
BeforeJson = JsonSerializer.Serialize(new { commissionBalance = before }),
|
|
AfterJson = JsonSerializer.Serialize(new { commissionBalance = before + input.Amount, input.Amount }),
|
|
Remark = input.Remark.Trim()
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// ?????????
|
|
/// </summary>
|
|
[HttpPost("/marketing/commission-withdraw/list")]
|
|
public async Task<OmsPageResult<CommissionWithdrawOutput>> GetWithdrawListAsync(CommissionWithdrawListInput input)
|
|
{
|
|
await ResolveWithdrawCustomerIdAsync(input);
|
|
var predicate = BuildWithdrawPredicate(input);
|
|
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
|
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
|
var entities = await WithdrawRepository.GetPagedListAsync(
|
|
predicate,
|
|
pageIndex,
|
|
pageSize,
|
|
new Dictionary<string, bool> { [nameof(CommissionWithdrawRequest.CreateTime)] = false, [nameof(CommissionWithdrawRequest.Id)] = false });
|
|
var total = await WithdrawRepository.CountAsync(predicate);
|
|
return new OmsPageResult<CommissionWithdrawOutput>
|
|
{
|
|
Items = await MapWithdrawOutputsAsync(entities),
|
|
Total = total
|
|
};
|
|
}
|
|
|
|
private async Task ResolveWithdrawCustomerIdAsync(CommissionWithdrawListInput input)
|
|
{
|
|
if (input.CustomerId is > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var code = input.CustomerCode?.Trim();
|
|
if (string.IsNullOrWhiteSpace(code))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var customer = await CustomerRepository.GetByCustomerCodeAsync(code)
|
|
?? throw new BusinessException($"???? {code} ???");
|
|
input.CustomerId = customer.ParentCustomerId ?? customer.Id;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ?????????
|
|
/// </summary>
|
|
[HttpPost("/marketing/commission-withdraw/approve")]
|
|
public async Task ApproveWithdrawAsync(CommissionWithdrawApproveInput input)
|
|
{
|
|
var request = await GetWithdrawOrThrowAsync(input.Id);
|
|
if (request.Status != WithdrawStatus.Pending)
|
|
{
|
|
throw new BusinessException("Only pending requests can be approved");
|
|
}
|
|
|
|
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == request.CustomerId)
|
|
?? throw new BusinessException("Customer not found");
|
|
|
|
if (request.WithdrawType == CommissionWithdrawType.ToBalance)
|
|
{
|
|
await CommissionBalanceMutator.DebitForTransferToBalanceAsync(
|
|
customer, request.Amount, request.Id, request.SerialNumber, input.AuditRemark ?? "Commission to balance");
|
|
}
|
|
else
|
|
{
|
|
await CommissionBalanceMutator.DebitForWithdrawAsync(
|
|
customer, request.Amount, request.Id, request.SerialNumber, input.AuditRemark ?? "Commission withdraw");
|
|
}
|
|
|
|
request.Status = WithdrawStatus.Approved;
|
|
request.AuditRemark = input.AuditRemark;
|
|
request.AuditBy = CurrentUser.UserName;
|
|
request.AuditTime = DateTime.Now;
|
|
await WithdrawRepository.UpdateAsync(request);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ???????
|
|
/// </summary>
|
|
[HttpPost("/marketing/commission-withdraw/reject")]
|
|
public async Task RejectWithdrawAsync(CommissionWithdrawRejectInput input)
|
|
{
|
|
var request = await GetWithdrawOrThrowAsync(input.Id);
|
|
if (request.Status != WithdrawStatus.Pending)
|
|
{
|
|
throw new BusinessException("Only pending requests can be rejected");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(input.AuditRemark))
|
|
{
|
|
throw new BusinessException("Reject reason is required");
|
|
}
|
|
|
|
request.Status = WithdrawStatus.Rejected;
|
|
request.AuditRemark = input.AuditRemark.Trim();
|
|
request.AuditBy = CurrentUser.UserName;
|
|
request.AuditTime = DateTime.Now;
|
|
await WithdrawRepository.UpdateAsync(request);
|
|
}
|
|
|
|
private static Expression<Func<ReferralCommissionDetail, bool>> BuildDetailPredicate(CommissionDetailListInput input)
|
|
{
|
|
var orderNo = input.OrderNo?.Trim();
|
|
var hasOrderNo = !string.IsNullOrWhiteSpace(orderNo);
|
|
var hasReferrer = input.ReferrerCustomerId is > 0;
|
|
var referrerId = input.ReferrerCustomerId.GetValueOrDefault();
|
|
var hasReferee = input.RefereeCustomerId is > 0;
|
|
var refereeId = input.RefereeCustomerId.GetValueOrDefault();
|
|
var status = input.Status;
|
|
var hasStatus = status.HasValue;
|
|
return x =>
|
|
(!hasReferrer || x.ReferrerCustomerId == referrerId) &&
|
|
(!hasReferee || x.RefereeCustomerId == refereeId) &&
|
|
(!hasStatus || x.Status == status!.Value) &&
|
|
(!hasOrderNo || x.OrderNo.Contains(orderNo!));
|
|
}
|
|
|
|
private static Expression<Func<CommissionWithdrawRequest, bool>> BuildWithdrawPredicate(CommissionWithdrawListInput input)
|
|
{
|
|
var hasCustomer = input.CustomerId is > 0;
|
|
var customerId = input.CustomerId.GetValueOrDefault();
|
|
var withdrawType = input.WithdrawType;
|
|
var hasWithdrawType = withdrawType.HasValue;
|
|
var status = input.Status;
|
|
var hasStatus = status.HasValue;
|
|
return x =>
|
|
(!hasCustomer || x.CustomerId == customerId) &&
|
|
(!hasWithdrawType || x.WithdrawType == withdrawType!.Value) &&
|
|
(!hasStatus || x.Status == status!.Value);
|
|
}
|
|
|
|
private async Task<List<CommissionDetailOutput>> MapDetailOutputsAsync(List<ReferralCommissionDetail> entities)
|
|
{
|
|
var customerIds = entities.SelectMany(x => new[] { x.ReferrerCustomerId, x.RefereeCustomerId }).Distinct().ToArray();
|
|
var customers = customerIds.Length == 0 ? [] : await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
|
|
var customerMap = customers.ToDictionary(x => x.Id);
|
|
return entities.Select(entity =>
|
|
{
|
|
customerMap.TryGetValue(entity.ReferrerCustomerId, out var referrer);
|
|
customerMap.TryGetValue(entity.RefereeCustomerId, out var referee);
|
|
return new CommissionDetailOutput
|
|
{
|
|
Id = entity.Id,
|
|
ReferrerCustomerId = entity.ReferrerCustomerId,
|
|
ReferrerCustomerCode = referrer?.CustomerCode,
|
|
RefereeCustomerId = entity.RefereeCustomerId,
|
|
RefereeCustomerCode = referee?.CustomerCode,
|
|
OrderId = entity.OrderId,
|
|
OrderNo = entity.OrderNo,
|
|
BasePayableAmount = entity.BasePayableAmount,
|
|
CommissionAmount = entity.CommissionAmount,
|
|
Status = entity.Status,
|
|
StatusName = MarketingEnumNames.Of(entity.Status),
|
|
EligibleTime = entity.EligibleTime,
|
|
SettleTime = entity.SettleTime,
|
|
CancelReason = entity.CancelReason,
|
|
CreateTime = entity.CreateTime
|
|
};
|
|
}).ToList();
|
|
}
|
|
|
|
private async Task<List<CommissionWithdrawOutput>> MapWithdrawOutputsAsync(List<CommissionWithdrawRequest> entities)
|
|
{
|
|
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
|
|
var customers = customerIds.Length == 0 ? [] : await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
|
|
var customerMap = customers.ToDictionary(x => x.Id);
|
|
return entities.Select(entity =>
|
|
{
|
|
customerMap.TryGetValue(entity.CustomerId, out var customer);
|
|
return new CommissionWithdrawOutput
|
|
{
|
|
Id = entity.Id,
|
|
CustomerId = entity.CustomerId,
|
|
CustomerCode = customer?.CustomerCode,
|
|
SerialNumber = entity.SerialNumber,
|
|
Amount = entity.Amount,
|
|
CommissionBalance = customer?.CommissionBalance ?? 0,
|
|
WithdrawType = entity.WithdrawType,
|
|
WithdrawTypeName = MarketingEnumNames.Of(entity.WithdrawType),
|
|
Status = entity.Status,
|
|
StatusName = MarketingEnumNames.Of(entity.Status),
|
|
Remark = entity.Remark,
|
|
AuditRemark = entity.AuditRemark,
|
|
AuditBy = entity.AuditBy,
|
|
AuditTime = entity.AuditTime,
|
|
CreateTime = entity.CreateTime
|
|
};
|
|
}).ToList();
|
|
}
|
|
|
|
internal static CommissionWithdrawOutput MapWithdrawOutput(
|
|
CommissionWithdrawRequest entity,
|
|
string? customerCode,
|
|
decimal commissionBalance = 0) => new()
|
|
{
|
|
Id = entity.Id,
|
|
CustomerId = entity.CustomerId,
|
|
CustomerCode = customerCode,
|
|
SerialNumber = entity.SerialNumber,
|
|
Amount = entity.Amount,
|
|
CommissionBalance = commissionBalance,
|
|
WithdrawType = entity.WithdrawType,
|
|
WithdrawTypeName = MarketingEnumNames.Of(entity.WithdrawType),
|
|
Status = entity.Status,
|
|
StatusName = MarketingEnumNames.Of(entity.Status),
|
|
Remark = entity.Remark,
|
|
AuditRemark = entity.AuditRemark,
|
|
AuditBy = entity.AuditBy,
|
|
AuditTime = entity.AuditTime,
|
|
CreateTime = entity.CreateTime
|
|
};
|
|
|
|
private async Task<Customer> ResolveCustomerAsync(
|
|
int? customerId,
|
|
string? customerCode)
|
|
{
|
|
if (customerId is > 0)
|
|
{
|
|
return await CustomerRepository.FirstOrDefaultAsync(x => x.Id == customerId.Value)
|
|
?? throw new BusinessException("Customer not found");
|
|
}
|
|
|
|
var code = customerCode?.Trim();
|
|
if (string.IsNullOrWhiteSpace(code))
|
|
{
|
|
throw new BusinessException("CustomerId or CustomerCode required");
|
|
}
|
|
|
|
return await CustomerRepository.GetByCustomerCodeAsync(code)
|
|
?? throw new BusinessException("Customer not found");
|
|
}
|
|
|
|
private async Task<CommissionWithdrawRequest> GetWithdrawOrThrowAsync(int id)
|
|
{
|
|
return await WithdrawRepository.FirstOrDefaultAsync(x => x.Id == id)
|
|
?? throw new BusinessException("Withdraw request not found");
|
|
}
|
|
}
|