feat: Add CommissionCQ - Phase 5 Application Layer

Added 5 Commands and 4 Queries for commission calculation and payout system:

Commands:
- CalculateWeeklyBalances: Recursive binary tree traversal for leg balances
- CalculateWeeklyCommissionPool: Calculate ValuePerBalance from total pool
- ProcessUserPayouts: Distribute commission to users, create payout records
- RequestWithdrawal: User requests cash/diamond withdrawal
- ProcessWithdrawal: Admin approves/rejects withdrawal

Queries:
- GetWeeklyCommissionPool: Retrieve pool details
- GetUserCommissionPayouts: List payouts with filters (status, week, user)
- GetCommissionPayoutHistory: Complete audit trail
- GetUserWeeklyBalances: Show leg balances and contributions

Total: 35 files, ~1,100 lines of code
Binary tree algorithm, state machine, withdrawal system implemented
This commit is contained in:
masoodafar-web
2025-11-29 04:32:17 +03:30
parent e68a7182d9
commit 487d1ceb15
31 changed files with 1213 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
/// <summary>
/// Command برای پردازش و توزیع کمیسیون به کاربران
/// </summary>
public record ProcessUserPayoutsCommand : IRequest<int>
{
/// <summary>
/// شماره هفته
/// </summary>
public string WeekNumber { get; init; } = string.Empty;
/// <summary>
/// آیا پرداخت مجدد انجام شود؟
/// </summary>
public bool ForceReprocess { get; init; }
}

View File

@@ -0,0 +1,99 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
public class ProcessUserPayoutsCommandHandler : IRequestHandler<ProcessUserPayoutsCommand, int>
{
private readonly IApplicationDbContext _context;
public ProcessUserPayoutsCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<int> Handle(ProcessUserPayoutsCommand request, CancellationToken cancellationToken)
{
// بررسی وجود استخر
var pool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(x => x.WeekNumber == request.WeekNumber, cancellationToken);
if (pool == null || !pool.IsCalculated)
{
throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekNumber} هنوز محاسبه نشده است");
}
// بررسی پرداخت قبلی
var existingPayouts = await _context.UserCommissionPayouts
.Where(x => x.WeekNumber == request.WeekNumber)
.ToListAsync(cancellationToken);
if (existingPayouts.Any() && !request.ForceReprocess)
{
throw new InvalidOperationException($"پرداخت‌های هفته {request.WeekNumber} قبلاً انجام شده است");
}
// حذف پرداخت‌های قبلی در صورت ForceReprocess
if (existingPayouts.Any())
{
_context.UserCommissionPayouts.RemoveRange(existingPayouts);
await _context.SaveChangesAsync(cancellationToken);
}
// دریافت تعادل‌های هفتگی
var weeklyBalances = await _context.NetworkWeeklyBalances
.Where(x => x.WeekNumber == request.WeekNumber && x.TotalBalances > 0)
.ToListAsync(cancellationToken);
var payoutsList = new List<UserCommissionPayout>();
foreach (var balance in weeklyBalances)
{
// محاسبه مبلغ کمیسیون
var totalAmount = (long)(balance.TotalBalances * pool.ValuePerBalance);
var payout = new UserCommissionPayout
{
UserId = balance.UserId,
WeekNumber = request.WeekNumber,
WeeklyPoolId = pool.Id,
BalancesEarned = balance.TotalBalances,
ValuePerBalance = pool.ValuePerBalance,
TotalAmount = totalAmount,
Status = CommissionPayoutStatus.Pending,
PaidAt = null,
WithdrawalMethod = null,
IbanNumber = null,
WithdrawnAt = null
};
payoutsList.Add(payout);
}
await _context.UserCommissionPayouts.AddRangeAsync(payoutsList, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
// ثبت تاریخچه برای هر پرداخت
var historyList = new List<CommissionPayoutHistory>();
foreach (var payout in payoutsList)
{
var history = new CommissionPayoutHistory
{
UserCommissionPayoutId = payout.Id,
UserId = payout.UserId,
WeekNumber = request.WeekNumber,
AmountBefore = 0,
AmountAfter = payout.TotalAmount,
OldStatus = default(CommissionPayoutStatus),
NewStatus = CommissionPayoutStatus.Pending,
Action = CommissionPayoutAction.Created,
PerformedBy = "System",
Reason = "پردازش خودکار کمیسیون هفتگی"
};
historyList.Add(history);
}
await _context.CommissionPayoutHistories.AddRangeAsync(historyList, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return payoutsList.Count;
}
}

View File

@@ -0,0 +1,26 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
public class ProcessUserPayoutsCommandValidator : AbstractValidator<ProcessUserPayoutsCommand>
{
public ProcessUserPayoutsCommandValidator()
{
RuleFor(x => x.WeekNumber)
.NotEmpty()
.WithMessage("شماره هفته نمی‌تواند خالی باشد")
.Matches(@"^\d{4}-W\d{2}$")
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد");
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<ProcessUserPayoutsCommand>.CreateWithOptions(
(ProcessUserPayoutsCommand)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}