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.CalculateWeeklyBalances;
/// <summary>
/// Command برای محاسبه تعادل‌های هفتگی شبکه
/// </summary>
public record CalculateWeeklyBalancesCommand : IRequest<int>
{
/// <summary>
/// شماره هفته (فرمت: YYYY-Www مثل 2025-W01)
/// </summary>
public string WeekNumber { get; init; } = string.Empty;
/// <summary>
/// آیا محاسبه مجدد انجام شود؟ (پیش‌فرض: false)
/// </summary>
public bool ForceRecalculate { get; init; }
}

View File

@@ -0,0 +1,95 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWeeklyBalancesCommand, int>
{
private readonly IApplicationDbContext _context;
public CalculateWeeklyBalancesCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<int> Handle(CalculateWeeklyBalancesCommand request, CancellationToken cancellationToken)
{
// بررسی وجود محاسبه قبلی
var existingBalances = await _context.NetworkWeeklyBalances
.Where(x => x.WeekNumber == request.WeekNumber)
.ToListAsync(cancellationToken);
if (existingBalances.Any() && !request.ForceRecalculate)
{
throw new InvalidOperationException($"تعادل‌های هفته {request.WeekNumber} قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید");
}
// حذف محاسبات قبلی در صورت ForceRecalculate
if (existingBalances.Any())
{
_context.NetworkWeeklyBalances.RemoveRange(existingBalances);
await _context.SaveChangesAsync(cancellationToken);
}
// دریافت کاربران فعال در شبکه
var usersInNetwork = await _context.Users
.Where(x => x.NetworkParentId.HasValue)
.Select(x => new { x.Id })
.ToListAsync(cancellationToken);
var balancesList = new List<NetworkWeeklyBalance>();
var calculatedAt = DateTime.UtcNow;
foreach (var user in usersInNetwork)
{
// محاسبه تعادل پای چپ (Left Leg)
var leftLegBalances = (int)await CalculateLegBalances(user.Id, NetworkLeg.Left, cancellationToken);
// محاسبه تعادل پای راست (Right Leg)
var rightLegBalances = (int)await CalculateLegBalances(user.Id, NetworkLeg.Right, cancellationToken);
// محاسبه Total Balances (کمترین مقدار دو پا)
var totalBalances = Math.Min(leftLegBalances, rightLegBalances);
// محاسبه سهم استخر (10% از Total Balances)
var weeklyPoolContribution = (long)(totalBalances * 0.10m);
var balance = new NetworkWeeklyBalance
{
UserId = user.Id,
WeekNumber = request.WeekNumber,
LeftLegBalances = leftLegBalances,
RightLegBalances = rightLegBalances,
TotalBalances = totalBalances,
WeeklyPoolContribution = weeklyPoolContribution,
CalculatedAt = calculatedAt,
IsExpired = false
};
balancesList.Add(balance);
}
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return balancesList.Count;
}
/// <summary>
/// محاسبه تعادل یک پا (Left یا Right) به صورت بازگشتی
/// </summary>
private async Task<long> CalculateLegBalances(long userId, NetworkLeg leg, CancellationToken cancellationToken)
{
// پیدا کردن فرزند در پای مورد نظر
var child = await _context.Users
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == leg, cancellationToken);
if (child == null)
{
return 0; // اگر فرزندی نداشته باشد، تعادل صفر است
}
// محاسبه بازگشتی: مجموع تعادل فرزند چپ + راست + 1 (خود فرزند)
var childLeftLeg = await CalculateLegBalances(child.Id, NetworkLeg.Left, cancellationToken);
var childRightLeg = await CalculateLegBalances(child.Id, NetworkLeg.Right, cancellationToken);
return 1 + childLeftLeg + childRightLeg;
}
}

View File

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

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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);
};
}

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);
};
}

View File

@@ -0,0 +1,22 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessWithdrawal;
/// <summary>
/// Command برای پردازش برداشت (توسط Admin)
/// </summary>
public record ProcessWithdrawalCommand : IRequest<Unit>
{
/// <summary>
/// شناسه پرداخت کمیسیون
/// </summary>
public long PayoutId { get; init; }
/// <summary>
/// آیا تایید شده است؟
/// </summary>
public bool IsApproved { get; init; }
/// <summary>
/// دلیل (در صورت رد)
/// </summary>
public string? Reason { get; init; }
}

View File

@@ -0,0 +1,102 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessWithdrawal;
public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawalCommand, Unit>
{
private readonly IApplicationDbContext _context;
public ProcessWithdrawalCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(ProcessWithdrawalCommand request, CancellationToken cancellationToken)
{
var payout = await _context.UserCommissionPayouts
.FirstOrDefaultAsync(x => x.Id == request.PayoutId, cancellationToken);
if (payout == null)
{
throw new NotFoundException(nameof(UserCommissionPayout), request.PayoutId);
}
// بررسی وضعیت
if (payout.Status != CommissionPayoutStatus.WithdrawRequested)
{
throw new InvalidOperationException($"فقط درخواست‌های با وضعیت WithdrawRequested قابل پردازش هستند. وضعیت فعلی: {payout.Status}");
}
var oldStatus = payout.Status;
var now = DateTime.UtcNow;
if (request.IsApproved)
{
// تایید برداشت
payout.Status = CommissionPayoutStatus.Withdrawn;
payout.WithdrawnAt = now;
// اگر روش برداشت Diamond بود، باید مبلغ به کیف پول تخفیف اضافه شود
if (payout.WithdrawalMethod == WithdrawalMethod.Diamond)
{
var wallet = await _context.UserWallets
.FirstOrDefaultAsync(x => x.UserId == payout.UserId, cancellationToken);
if (wallet != null)
{
wallet.DiscountBalance += payout.TotalAmount;
_context.UserWallets.Update(wallet);
}
}
_context.UserCommissionPayouts.Update(payout);
await _context.SaveChangesAsync(cancellationToken);
// ثبت تاریخچه
var history = new CommissionPayoutHistory
{
UserCommissionPayoutId = payout.Id,
UserId = payout.UserId,
WeekNumber = payout.WeekNumber,
AmountBefore = payout.TotalAmount,
AmountAfter = payout.TotalAmount,
OldStatus = oldStatus,
NewStatus = CommissionPayoutStatus.Withdrawn,
Action = CommissionPayoutAction.Withdrawn,
PerformedBy = "Admin", // TODO: باید از Current User گرفته شود
Reason = $"تایید برداشت به روش {payout.WithdrawalMethod}"
};
await _context.CommissionPayoutHistories.AddAsync(history, cancellationToken);
}
else
{
// رد برداشت - برگشت به وضعیت Paid
payout.Status = CommissionPayoutStatus.Paid;
payout.WithdrawalMethod = null;
payout.IbanNumber = null;
_context.UserCommissionPayouts.Update(payout);
await _context.SaveChangesAsync(cancellationToken);
// ثبت تاریخچه
var history = new CommissionPayoutHistory
{
UserCommissionPayoutId = payout.Id,
UserId = payout.UserId,
WeekNumber = payout.WeekNumber,
AmountBefore = payout.TotalAmount,
AmountAfter = payout.TotalAmount,
OldStatus = oldStatus,
NewStatus = CommissionPayoutStatus.Paid,
Action = CommissionPayoutAction.Cancelled,
PerformedBy = "Admin", // TODO: باید از Current User گرفته شود
Reason = request.Reason ?? "درخواست برداشت رد شد"
};
await _context.CommissionPayoutHistories.AddAsync(history, cancellationToken);
}
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}

View File

@@ -0,0 +1,31 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessWithdrawal;
public class ProcessWithdrawalCommandValidator : AbstractValidator<ProcessWithdrawalCommand>
{
public ProcessWithdrawalCommandValidator()
{
RuleFor(x => x.PayoutId)
.GreaterThan(0)
.WithMessage("شناسه پرداخت معتبر نیست");
RuleFor(x => x.Reason)
.NotEmpty()
.WithMessage("دلیل رد الزامی است")
.MaximumLength(500)
.WithMessage("طول دلیل نباید بیشتر از 500 کاراکتر باشد")
.When(x => !x.IsApproved);
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<ProcessWithdrawalCommand>.CreateWithOptions(
(ProcessWithdrawalCommand)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}

View File

@@ -0,0 +1,22 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
/// <summary>
/// Command برای درخواست برداشت کمیسیون
/// </summary>
public record RequestWithdrawalCommand : IRequest<Unit>
{
/// <summary>
/// شناسه پرداخت کمیسیون
/// </summary>
public long PayoutId { get; init; }
/// <summary>
/// روش برداشت (Cash یا Diamond)
/// </summary>
public WithdrawalMethod Method { get; init; }
/// <summary>
/// شماره شبا (برای Cash)
/// </summary>
public string? IbanNumber { get; init; }
}

View File

@@ -0,0 +1,62 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawalCommand, Unit>
{
private readonly IApplicationDbContext _context;
public RequestWithdrawalCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Unit> Handle(RequestWithdrawalCommand request, CancellationToken cancellationToken)
{
var payout = await _context.UserCommissionPayouts
.FirstOrDefaultAsync(x => x.Id == request.PayoutId, cancellationToken);
if (payout == null)
{
throw new NotFoundException(nameof(UserCommissionPayout), request.PayoutId);
}
// بررسی وضعیت
if (payout.Status != CommissionPayoutStatus.Paid)
{
throw new InvalidOperationException($"فقط پرداخت‌های با وضعیت Paid قابل برداشت هستند. وضعیت فعلی: {payout.Status}");
}
var oldStatus = payout.Status;
// به‌روزرسانی وضعیت
payout.Status = CommissionPayoutStatus.WithdrawRequested;
payout.WithdrawalMethod = request.Method;
if (request.Method == WithdrawalMethod.Cash)
{
payout.IbanNumber = request.IbanNumber;
}
_context.UserCommissionPayouts.Update(payout);
await _context.SaveChangesAsync(cancellationToken);
// ثبت تاریخچه
var history = new CommissionPayoutHistory
{
UserCommissionPayoutId = payout.Id,
UserId = payout.UserId,
WeekNumber = payout.WeekNumber,
AmountBefore = payout.TotalAmount,
AmountAfter = payout.TotalAmount,
OldStatus = oldStatus,
NewStatus = CommissionPayoutStatus.WithdrawRequested,
Action = CommissionPayoutAction.WithdrawRequested,
PerformedBy = "User", // TODO: باید از Current User گرفته شود
Reason = $"درخواست برداشت به روش {request.Method}"
};
await _context.CommissionPayoutHistories.AddAsync(history, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return Unit.Value;
}
}

View File

@@ -0,0 +1,35 @@
namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
public class RequestWithdrawalCommandValidator : AbstractValidator<RequestWithdrawalCommand>
{
public RequestWithdrawalCommandValidator()
{
RuleFor(x => x.PayoutId)
.GreaterThan(0)
.WithMessage("شناسه پرداخت معتبر نیست");
RuleFor(x => x.Method)
.IsInEnum()
.WithMessage("روش برداشت باید Cash یا Diamond باشد");
RuleFor(x => x.IbanNumber)
.NotEmpty()
.WithMessage("شماره شبا الزامی است")
.Matches(@"^IR\d{24}$")
.WithMessage("فرمت شماره شبا معتبر نیست (IR + 24 رقم)")
.When(x => x.Method == WithdrawalMethod.Cash);
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(
ValidationContext<RequestWithdrawalCommand>.CreateWithOptions(
(RequestWithdrawalCommand)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}

View File

@@ -0,0 +1,32 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHistory;
/// <summary>
/// Query برای دریافت تاریخچه تغییرات کمیسیون
/// </summary>
public record GetCommissionPayoutHistoryQuery : IRequest<GetCommissionPayoutHistoryResponseDto>
{
/// <summary>
/// شناسه پرداخت (اختیاری)
/// </summary>
public long? PayoutId { get; init; }
/// <summary>
/// شناسه کاربر (اختیاری)
/// </summary>
public long? UserId { get; init; }
/// <summary>
/// شماره هفته (اختیاری)
/// </summary>
public string? WeekNumber { get; init; }
/// <summary>
/// مرتب‌سازی
/// </summary>
public string? SortBy { get; init; }
/// <summary>
/// Pagination
/// </summary>
public PaginationState? PaginationState { get; init; }
}

View File

@@ -0,0 +1,63 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHistory;
public class GetCommissionPayoutHistoryQueryHandler : IRequestHandler<GetCommissionPayoutHistoryQuery, GetCommissionPayoutHistoryResponseDto>
{
private readonly IApplicationDbContext _context;
public GetCommissionPayoutHistoryQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetCommissionPayoutHistoryResponseDto> Handle(GetCommissionPayoutHistoryQuery request, CancellationToken cancellationToken)
{
var query = _context.CommissionPayoutHistories
.AsNoTracking()
.AsQueryable();
// فیلترها
if (request.PayoutId.HasValue)
{
query = query.Where(x => x.UserCommissionPayoutId == request.PayoutId.Value);
}
if (request.UserId.HasValue)
{
query = query.Where(x => x.UserId == request.UserId.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 GetCommissionPayoutHistoryResponseModel
{
Id = x.Id,
UserCommissionPayoutId = x.UserCommissionPayoutId,
UserId = x.UserId,
WeekNumber = x.WeekNumber,
AmountBefore = x.AmountBefore,
AmountAfter = x.AmountAfter,
OldStatus = x.OldStatus,
NewStatus = x.NewStatus,
Action = x.Action,
PerformedBy = x.PerformedBy,
Reason = x.Reason,
Created = x.Created
})
.ToListAsync(cancellationToken);
return new GetCommissionPayoutHistoryResponseDto
{
MetaData = meta,
Models = models
};
}
}

View File

@@ -0,0 +1,35 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHistory;
public class GetCommissionPayoutHistoryQueryValidator : AbstractValidator<GetCommissionPayoutHistoryQuery>
{
public GetCommissionPayoutHistoryQueryValidator()
{
RuleFor(x => x.PayoutId)
.GreaterThan(0)
.WithMessage("شناسه پرداخت معتبر نیست")
.When(x => x.PayoutId.HasValue);
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر معتبر نیست")
.When(x => x.UserId.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<GetCommissionPayoutHistoryQuery>.CreateWithOptions(
(GetCommissionPayoutHistoryQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}

View File

@@ -0,0 +1,23 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHistory;
public class GetCommissionPayoutHistoryResponseDto
{
public MetaData MetaData { get; set; }
public List<GetCommissionPayoutHistoryResponseModel> Models { get; set; }
}
public class GetCommissionPayoutHistoryResponseModel
{
public long Id { get; set; }
public long UserCommissionPayoutId { get; set; }
public long UserId { get; set; }
public string WeekNumber { get; set; } = string.Empty;
public long AmountBefore { get; set; }
public long AmountAfter { get; set; }
public CommissionPayoutStatus? OldStatus { get; set; }
public CommissionPayoutStatus NewStatus { get; set; }
public CommissionPayoutAction Action { get; set; }
public string? PerformedBy { get; set; }
public string? Reason { get; set; }
public DateTimeOffset Created { get; set; }
}

View File

@@ -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; }
}

View File

@@ -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
};
}
}

View File

@@ -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);
};
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,32 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
/// <summary>
/// Query برای دریافت تعادل‌های هفتگی کاربر
/// </summary>
public record GetUserWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesResponseDto>
{
/// <summary>
/// شناسه کاربر (اختیاری)
/// </summary>
public long? UserId { get; init; }
/// <summary>
/// شماره هفته (اختیاری)
/// </summary>
public string? WeekNumber { get; init; }
/// <summary>
/// فقط موارد Expired نشده؟
/// </summary>
public bool? OnlyActive { get; init; }
/// <summary>
/// مرتب‌سازی
/// </summary>
public string? SortBy { get; init; }
/// <summary>
/// Pagination
/// </summary>
public PaginationState? PaginationState { get; init; }
}

View File

@@ -0,0 +1,61 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBalancesQuery, GetUserWeeklyBalancesResponseDto>
{
private readonly IApplicationDbContext _context;
public GetUserWeeklyBalancesQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetUserWeeklyBalancesQuery request, CancellationToken cancellationToken)
{
var query = _context.NetworkWeeklyBalances
.AsNoTracking()
.AsQueryable();
// فیلترها
if (request.UserId.HasValue)
{
query = query.Where(x => x.UserId == request.UserId.Value);
}
if (!string.IsNullOrEmpty(request.WeekNumber))
{
query = query.Where(x => x.WeekNumber == request.WeekNumber);
}
if (request.OnlyActive.HasValue && request.OnlyActive.Value)
{
query = query.Where(x => !x.IsExpired);
}
query = query.ApplyOrder(sortBy: request.SortBy ?? "-WeekNumber");
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
var models = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.Select(x => new GetUserWeeklyBalancesResponseModel
{
Id = x.Id,
UserId = x.UserId,
WeekNumber = x.WeekNumber,
LeftLegBalances = x.LeftLegBalances,
RightLegBalances = x.RightLegBalances,
TotalBalances = x.TotalBalances,
WeeklyPoolContribution = x.WeeklyPoolContribution,
CalculatedAt = x.CalculatedAt,
IsExpired = x.IsExpired,
Created = x.Created
})
.ToListAsync(cancellationToken);
return new GetUserWeeklyBalancesResponseDto
{
MetaData = meta,
Models = models
};
}
}

View File

@@ -0,0 +1,30 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
public class GetUserWeeklyBalancesQueryValidator : AbstractValidator<GetUserWeeklyBalancesQuery>
{
public GetUserWeeklyBalancesQueryValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر معتبر نیست")
.When(x => x.UserId.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<GetUserWeeklyBalancesQuery>.CreateWithOptions(
(GetUserWeeklyBalancesQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}

View File

@@ -0,0 +1,21 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
public class GetUserWeeklyBalancesResponseDto
{
public MetaData MetaData { get; set; }
public List<GetUserWeeklyBalancesResponseModel> Models { get; set; }
}
public class GetUserWeeklyBalancesResponseModel
{
public long Id { get; set; }
public long UserId { get; set; }
public string WeekNumber { get; set; } = string.Empty;
public int LeftLegBalances { get; set; }
public int RightLegBalances { get; set; }
public int TotalBalances { get; set; }
public long WeeklyPoolContribution { get; set; }
public DateTime? CalculatedAt { get; set; }
public bool IsExpired { get; set; }
public DateTimeOffset Created { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPool;
/// <summary>
/// Query برای دریافت استخر کمیسیون هفتگی
/// </summary>
public record GetWeeklyCommissionPoolQuery : IRequest<WeeklyCommissionPoolDto?>
{
/// <summary>
/// شماره هفته
/// </summary>
public string WeekNumber { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,32 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPool;
public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler<GetWeeklyCommissionPoolQuery, WeeklyCommissionPoolDto?>
{
private readonly IApplicationDbContext _context;
public GetWeeklyCommissionPoolQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<WeeklyCommissionPoolDto?> Handle(GetWeeklyCommissionPoolQuery request, CancellationToken cancellationToken)
{
var pool = await _context.WeeklyCommissionPools
.AsNoTracking()
.Where(x => x.WeekNumber == request.WeekNumber)
.Select(x => new WeeklyCommissionPoolDto
{
Id = x.Id,
WeekNumber = x.WeekNumber,
TotalPoolAmount = x.TotalPoolAmount,
TotalBalances = x.TotalBalances,
ValuePerBalance = x.ValuePerBalance,
IsCalculated = x.IsCalculated,
CalculatedAt = x.CalculatedAt,
Created = x.Created
})
.FirstOrDefaultAsync(cancellationToken);
return pool;
}
}

View File

@@ -0,0 +1,26 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPool;
public class GetWeeklyCommissionPoolQueryValidator : AbstractValidator<GetWeeklyCommissionPoolQuery>
{
public GetWeeklyCommissionPoolQueryValidator()
{
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<GetWeeklyCommissionPoolQuery>.CreateWithOptions(
(GetWeeklyCommissionPoolQuery)model,
x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}

View File

@@ -0,0 +1,16 @@
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPool;
/// <summary>
/// DTO برای استخر کمیسیون هفتگی
/// </summary>
public class WeeklyCommissionPoolDto
{
public long Id { get; set; }
public string WeekNumber { get; set; } = string.Empty;
public long TotalPoolAmount { get; set; }
public long TotalBalances { get; set; }
public decimal ValuePerBalance { get; set; }
public bool IsCalculated { get; set; }
public DateTime? CalculatedAt { get; set; }
public DateTimeOffset Created { get; set; }
}