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:
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user