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,32 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
/// <summary>
/// Query برای دریافت پرداخت‌های کمیسیون کاربر
/// </summary>
public record GetUserCommissionPayoutsQuery : IRequest<GetUserCommissionPayoutsResponseDto>
{
/// <summary>
/// شناسه کاربر (اختیاری)
/// </summary>
public long? UserId { get; init; }
/// <summary>
/// فیلتر وضعیت
/// </summary>
public CommissionPayoutStatus? Status { get; init; }
/// <summary>
/// شماره هفته (اختیاری)
/// </summary>
public string? WeekNumber { get; init; }
/// <summary>
/// مرتب‌سازی
/// </summary>
public string? SortBy { get; init; }
/// <summary>
/// Pagination
/// </summary>
public PaginationState? PaginationState { get; init; }
}
@@ -0,0 +1,64 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommissionPayoutsQuery, GetUserCommissionPayoutsResponseDto>
{
private readonly IApplicationDbContext _context;
public GetUserCommissionPayoutsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetUserCommissionPayoutsResponseDto> Handle(GetUserCommissionPayoutsQuery request, CancellationToken cancellationToken)
{
var query = _context.UserCommissionPayouts
.AsNoTracking()
.AsQueryable();
// فیلترها
if (request.UserId.HasValue)
{
query = query.Where(x => x.UserId == request.UserId.Value);
}
if (request.Status.HasValue)
{
query = query.Where(x => x.Status == request.Status.Value);
}
if (!string.IsNullOrEmpty(request.WeekNumber))
{
query = query.Where(x => x.WeekNumber == request.WeekNumber);
}
query = query.ApplyOrder(sortBy: request.SortBy ?? "-Created");
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
var models = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.Select(x => new GetUserCommissionPayoutsResponseModel
{
Id = x.Id,
UserId = x.UserId,
WeekNumber = x.WeekNumber,
WeeklyPoolId = x.WeeklyPoolId,
BalancesEarned = x.BalancesEarned,
ValuePerBalance = x.ValuePerBalance,
TotalAmount = x.TotalAmount,
Status = x.Status,
PaidAt = x.PaidAt,
WithdrawalMethod = x.WithdrawalMethod,
IbanNumber = x.IbanNumber,
WithdrawnAt = x.WithdrawnAt,
Created = x.Created
})
.ToListAsync(cancellationToken);
return new GetUserCommissionPayoutsResponseDto
{
MetaData = meta,
Models = models
};
}
}
@@ -0,0 +1,35 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
public class GetUserCommissionPayoutsQueryValidator : AbstractValidator<GetUserCommissionPayoutsQuery>
{
public GetUserCommissionPayoutsQueryValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر معتبر نیست")
.When(x => x.UserId.HasValue);
RuleFor(x => x.Status)
.IsInEnum()
.WithMessage("وضعیت معتبر نیست")
.When(x => x.Status.HasValue);
RuleFor(x => x.WeekNumber)
.Matches(@"^\d{4}-W\d{2}$")
.WithMessage("فرمت شماره هفته باید YYYY-Www باشد")
.When(x => !string.IsNullOrEmpty(x.WeekNumber));
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<GetUserCommissionPayoutsQuery>.CreateWithOptions(
(GetUserCommissionPayoutsQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -0,0 +1,24 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
public class GetUserCommissionPayoutsResponseDto
{
public MetaData MetaData { get; set; }
public List<GetUserCommissionPayoutsResponseModel> Models { get; set; }
}
public class GetUserCommissionPayoutsResponseModel
{
public long Id { get; set; }
public long UserId { get; set; }
public string WeekNumber { get; set; } = string.Empty;
public long WeeklyPoolId { get; set; }
public long BalancesEarned { get; set; }
public decimal ValuePerBalance { get; set; }
public long TotalAmount { get; set; }
public CommissionPayoutStatus Status { get; set; }
public DateTime? PaidAt { get; set; }
public WithdrawalMethod? WithdrawalMethod { get; set; }
public string? IbanNumber { get; set; }
public DateTime? WithdrawnAt { get; set; }
public DateTimeOffset Created { get; set; }
}