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; /// /// ????????? API?????? /// [ApiExplorerSettings(GroupName = "OMS-Marketing")] public class MarketingCommissionAdminService : ApplicationService { private IReferralCommissionDetailRepository DetailRepository => LazyServiceProvider.GetRequiredService(); private ICommissionWithdrawRequestRepository WithdrawRepository => LazyServiceProvider.GetRequiredService(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService(); private IMarketingAuditLogRepository AuditLogRepository => LazyServiceProvider.GetRequiredService(); private CommissionBalanceMutator CommissionBalanceMutator => LazyServiceProvider.GetRequiredService(); /// /// ????????? /// [HttpPost("/marketing/commission-detail/list")] public async Task> 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 { [nameof(ReferralCommissionDetail.CreateTime)] = false, [nameof(ReferralCommissionDetail.Id)] = false }); var total = await DetailRepository.CountAsync(predicate); return new OmsPageResult { Items = await MapDetailOutputsAsync(entities), Total = total }; } /// /// ????????? /// [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(), 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() }); } /// /// ????????? /// [HttpPost("/marketing/commission-withdraw/list")] public async Task> 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 { [nameof(CommissionWithdrawRequest.CreateTime)] = false, [nameof(CommissionWithdrawRequest.Id)] = false }); var total = await WithdrawRepository.CountAsync(predicate); return new OmsPageResult { 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; } /// /// ????????? /// [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); } /// /// ??????? /// [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> 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> 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> MapDetailOutputsAsync(List 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> MapWithdrawOutputsAsync(List 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 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 GetWithdrawOrThrowAsync(int id) { return await WithdrawRepository.FirstOrDefaultAsync(x => x.Id == id) ?? throw new BusinessException("Withdraw request not found"); } }