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
@@ -0,0 +1,17 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
/// <summary>
/// Command برای محاسبه استخر کمیسیون هفتگی
/// </summary>
public record CalculateWeeklyCommissionPoolCommand : IRequest<long>
{
/// <summary>
/// شماره هفته (فرمت: YYYY-Www)
/// </summary>
public string WeekNumber { get; init; } = string.Empty;
/// <summary>
/// آیا محاسبه مجدد انجام شود؟
/// </summary>
public bool ForceRecalculate { get; init; }
}
@@ -0,0 +1,78 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
public class CalculateWeeklyCommissionPoolCommandHandler : IRequestHandler<CalculateWeeklyCommissionPoolCommand, long>
{
private readonly IApplicationDbContext _context;
public CalculateWeeklyCommissionPoolCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<long> Handle(CalculateWeeklyCommissionPoolCommand request, CancellationToken cancellationToken)
{
// بررسی وجود استخر قبلی
var existingPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(x => x.WeekNumber == request.WeekNumber, cancellationToken);
if (existingPool != null && existingPool.IsCalculated && !request.ForceRecalculate)
{
throw new InvalidOperationException($"استخر کمیسیون هفته {request.WeekNumber} قبلاً محاسبه شده است");
}
// بررسی وجود تعادل‌های هفتگی
var weeklyBalances = await _context.NetworkWeeklyBalances
.Where(x => x.WeekNumber == request.WeekNumber)
.ToListAsync(cancellationToken);
if (!weeklyBalances.Any())
{
throw new InvalidOperationException($"تعادل‌های هفته {request.WeekNumber} هنوز محاسبه نشده است. ابتدا CalculateWeeklyBalances را اجرا کنید");
}
// محاسبه مجموع مشارکت‌ها در استخر
var totalPoolAmount = weeklyBalances.Sum(x => x.WeeklyPoolContribution);
// محاسبه مجموع Balances
var totalBalances = weeklyBalances.Sum(x => x.TotalBalances);
// محاسبه ارزش هر Balance (تقسیم صحیح برای ریال)
long valuePerBalance = 0;
if (totalBalances > 0)
{
valuePerBalance = totalPoolAmount / totalBalances;
}
if (existingPool != null)
{
// به‌روزرسانی
existingPool.TotalPoolAmount = totalPoolAmount;
existingPool.TotalBalances = totalBalances;
existingPool.ValuePerBalance = valuePerBalance;
existingPool.IsCalculated = true;
existingPool.CalculatedAt = DateTime.UtcNow;
_context.WeeklyCommissionPools.Update(existingPool);
}
else
{
// ایجاد جدید
var pool = new WeeklyCommissionPool
{
WeekNumber = request.WeekNumber,
TotalPoolAmount = totalPoolAmount,
TotalBalances = totalBalances,
ValuePerBalance = valuePerBalance,
IsCalculated = true,
CalculatedAt = DateTime.UtcNow
};
await _context.WeeklyCommissionPools.AddAsync(pool, cancellationToken);
existingPool = pool;
}
await _context.SaveChangesAsync(cancellationToken);
return existingPool.Id;
}
}
@@ -0,0 +1,26 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
public class CalculateWeeklyCommissionPoolCommandValidator : AbstractValidator<CalculateWeeklyCommissionPoolCommand>
{
public CalculateWeeklyCommissionPoolCommandValidator()
{
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<CalculateWeeklyCommissionPoolCommand>.CreateWithOptions(
(CalculateWeeklyCommissionPoolCommand)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}