feat: Enhance network membership and withdrawal processing with user tracking and logging

This commit is contained in:
masoodafar-web
2025-12-01 20:52:18 +03:30
parent 4aaf2247ff
commit 25fc73ae28
47 changed files with 9545 additions and 284 deletions

View File

@@ -6,10 +6,14 @@ namespace CMSMicroservice.Application.CommissionCQ.Commands.ApproveWithdrawal;
public class ApproveWithdrawalCommandHandler : IRequestHandler<ApproveWithdrawalCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
public ApproveWithdrawalCommandHandler(IApplicationDbContext context)
public ApproveWithdrawalCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<Unit> Handle(ApproveWithdrawalCommand request, CancellationToken cancellationToken)
@@ -30,6 +34,8 @@ public class ApproveWithdrawalCommandHandler : IRequestHandler<ApproveWithdrawal
// Update status to Withdrawn (approved)
payout.Status = CommissionPayoutStatus.Withdrawn;
payout.WithdrawnAt = DateTime.UtcNow;
payout.ProcessedBy = _currentUser.GetPerformedBy();
payout.ProcessedAt = DateTime.UtcNow;
payout.LastModified = DateTime.UtcNow;
// TODO: Add PayoutHistory record

View File

@@ -34,30 +34,95 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
.Select(x => new { x.Id })
.ToListAsync(cancellationToken);
// دریافت باقیمانده‌های هفته قبل
var previousWeekNumber = GetPreviousWeekNumber(request.WeekNumber);
var previousWeekCarryovers = await _context.NetworkWeeklyBalances
.Where(x => x.WeekNumber == previousWeekNumber)
.Select(x => new
{
x.UserId,
x.LeftLegRemainder,
x.RightLegRemainder
})
.ToDictionaryAsync(x => x.UserId, cancellationToken);
var balancesList = new List<NetworkWeeklyBalance>();
var calculatedAt = DateTime.UtcNow;
// خواندن یکباره Configuration ها (بهینه‌سازی - به جای N query)
var configs = await _context.SystemConfigurations
.Where(x => x.IsActive && (
x.Key == "Club.ActivationFee" ||
x.Key == "Commission.WeeklyPoolContributionPercent" ||
x.Key == "Commission.MaxWeeklyBalancesPerUser"))
.ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken);
var activationFee = long.Parse(configs.GetValueOrDefault("Club.ActivationFee", "25000000"));
var poolPercent = decimal.Parse(configs.GetValueOrDefault("Commission.WeeklyPoolContributionPercent", "20")) / 100m;
var maxWeeklyBalances = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerUser", "300"));
foreach (var user in usersInNetwork)
{
// محاسبه تعادل پای چپ (Left Leg)
var leftLegBalances = (int)await CalculateLegBalances(user.Id, NetworkLeg.Left, cancellationToken);
// دریافت باقیمانده هفته قبل
var leftCarryover = 0;
var rightCarryover = 0;
if (previousWeekCarryovers.ContainsKey(user.Id))
{
leftCarryover = previousWeekCarryovers[user.Id].LeftLegRemainder;
rightCarryover = previousWeekCarryovers[user.Id].RightLegRemainder;
}
// محاسبه تعادل پای راست (Right Leg)
var rightLegBalances = (int)await CalculateLegBalances(user.Id, NetworkLeg.Right, cancellationToken);
// محاسبه تعداد اعضای جدید در این هفته (فقط فرزندان مستقیم که در این هفته فعال شدند)
var leftNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Left, request.WeekNumber, cancellationToken);
var rightNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Right, request.WeekNumber, cancellationToken);
// محاسبه Total Balances (کمترین مقدار دو پا)
var totalBalances = Math.Min(leftLegBalances, rightLegBalances);
// محاسبه مجموع هر پا (جدید + باقیمانده)
var leftTotal = leftNewMembers + leftCarryover;
var rightTotal = rightNewMembers + rightCarryover;
// محاسبه سهم استخر (10% از Total Balances)
var weeklyPoolContribution = (long)(totalBalances * 0.10m);
// محاسبه تعادل (کمترین مقدار)
var totalBalances = Math.Min(leftTotal, rightTotal);
// اعمال محدودیت سقف تعادل هفتگی (مثلاً 300)
var cappedBalances = Math.Min(totalBalances, maxWeeklyBalances);
// محاسبه باقیمانده برای هفته بعد
// اگر تعادل بیش از سقف بود، مازاد هم به remainder اضافه می‌شود
var excessBalances = totalBalances - cappedBalances;
var leftRemainder = (leftTotal - totalBalances) + (leftTotal >= rightTotal ? excessBalances : 0);
var rightRemainder = (rightTotal - totalBalances) + (rightTotal >= leftTotal ? excessBalances : 0);
// محاسبه سهم استخر (20% از مجموع فعال‌سازی‌های جدید کل شبکه)
// طبق گفته دکتر: کل افراد جدید در شبکه × هزینه فعال‌سازی × 20%
var totalNewMembers = leftNewMembers + rightNewMembers;
var weeklyPoolContribution = (long)(totalNewMembers * activationFee * poolPercent);
var balance = new NetworkWeeklyBalance
{
UserId = user.Id,
WeekNumber = request.WeekNumber,
LeftLegBalances = leftLegBalances,
RightLegBalances = rightLegBalances,
TotalBalances = totalBalances,
// اطلاعات جدید
LeftLegNewMembers = leftNewMembers,
RightLegNewMembers = rightNewMembers,
LeftLegCarryover = leftCarryover,
RightLegCarryover = rightCarryover,
// مجموع
LeftLegTotal = leftTotal,
RightLegTotal = rightTotal,
TotalBalances = cappedBalances, // تعادل واقعی بعد از اعمال سقف
// باقیمانده برای هفته بعد
LeftLegRemainder = leftRemainder,
RightLegRemainder = rightRemainder,
// فیلدهای قدیمی (deprecated) - برای سازگاری با کدهای قبلی
#pragma warning disable CS0618
LeftLegBalances = leftTotal,
RightLegBalances = rightTotal,
#pragma warning restore CS0618
WeeklyPoolContribution = weeklyPoolContribution,
CalculatedAt = calculatedAt,
IsExpired = false
@@ -73,23 +138,89 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
}
/// <summary>
/// محاسبه تعادل یک پا (Left یا Right) به صورت بازگشتی
/// شماره هفته قبل را محاسبه می‌کند
/// </summary>
private async Task<long> CalculateLegBalances(long userId, NetworkLeg leg, CancellationToken cancellationToken)
private string GetPreviousWeekNumber(string currentWeekNumber)
{
// پیدا کردن فرزند در پای مورد نظر
// مثال: "2025-W48" -> "2025-W47"
var parts = currentWeekNumber.Split('-');
var year = int.Parse(parts[0]);
var week = int.Parse(parts[1].Replace("W", ""));
week--;
if (week < 1)
{
year--;
week = 52; // یا 53 بسته به سال
}
return $"{year}-W{week:D2}";
}
/// <summary>
/// شمارش اعضای جدیدی که در این هفته به یک پا اضافه شدند
/// فقط فرزندان مستقیم که ActivatedAt آنها در این هفته است
/// </summary>
private async Task<int> CountNewMembersInLeg(long userId, NetworkLeg leg, string weekNumber, CancellationToken cancellationToken)
{
// تبدیل WeekNumber به بازه تاریخی
var (startDate, endDate) = GetWeekDateRange(weekNumber);
// شمارش تمام اعضای زیرمجموعه که در این هفته فعال شدند
var count = await CountNewMembersRecursive(userId, leg, startDate, endDate, cancellationToken);
return count;
}
/// <summary>
/// شمارش بازگشتی اعضای جدید در یک پا
/// </summary>
private async Task<int> CountNewMembersRecursive(long userId, NetworkLeg leg, DateTime startDate, DateTime endDate, CancellationToken cancellationToken)
{
// پیدا کردن فرزند مستقیم در پای مورد نظر
var child = await _context.Users
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == leg, cancellationToken);
if (child == null)
{
return 0; // اگر فرزندی نداشته باشد، تعادل صفر است
return 0;
}
// محاسبه بازگشتی: مجموع تعادل فرزند چپ + راست + 1 (خود فرزند)
var childLeftLeg = await CalculateLegBalances(child.Id, NetworkLeg.Left, cancellationToken);
var childRightLeg = await CalculateLegBalances(child.Id, NetworkLeg.Right, cancellationToken);
var count = 0;
return 1 + childLeftLeg + childRightLeg;
// اگر فرزند در این هفته فعال شده، 1 امتیاز
var membership = await _context.ClubMemberships
.FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsActive, cancellationToken);
if (membership?.ActivatedAt >= startDate && membership?.ActivatedAt <= endDate)
{
count = 1;
}
// جمع کردن اعضای جدید از پای چپ و راست فرزند
var childLeft = await CountNewMembersRecursive(child.Id, NetworkLeg.Left, startDate, endDate, cancellationToken);
var childRight = await CountNewMembersRecursive(child.Id, NetworkLeg.Right, startDate, endDate, cancellationToken);
return count + childLeft + childRight;
}
/// <summary>
/// تبدیل شماره هفته به بازه تاریخی
/// </summary>
private (DateTime startDate, DateTime endDate) GetWeekDateRange(string weekNumber)
{
// مثال: "2025-W48"
var parts = weekNumber.Split('-');
var year = int.Parse(parts[0]);
var week = int.Parse(parts[1].Replace("W", ""));
// محاسبه اولین روز هفته (شنبه)
var jan1 = new DateTime(year, 1, 1);
var daysOffset = DayOfWeek.Saturday - jan1.DayOfWeek;
var firstSaturday = jan1.AddDays(daysOffset);
var weekStart = firstSaturday.AddDays((week - 1) * 7);
var weekEnd = weekStart.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
return (weekStart, weekEnd);
}
}

View File

@@ -3,10 +3,14 @@ namespace CMSMicroservice.Application.CommissionCQ.Commands.ProcessWithdrawal;
public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawalCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
public ProcessWithdrawalCommandHandler(IApplicationDbContext context)
public ProcessWithdrawalCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<Unit> Handle(ProcessWithdrawalCommand request, CancellationToken cancellationToken)

View File

@@ -6,10 +6,14 @@ namespace CMSMicroservice.Application.CommissionCQ.Commands.RejectWithdrawal;
public class RejectWithdrawalCommandHandler : IRequestHandler<RejectWithdrawalCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
public RejectWithdrawalCommandHandler(IApplicationDbContext context)
public RejectWithdrawalCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<Unit> Handle(RejectWithdrawalCommand request, CancellationToken cancellationToken)
@@ -29,6 +33,9 @@ public class RejectWithdrawalCommandHandler : IRequestHandler<RejectWithdrawalCo
// Update status to Cancelled (rejected)
payout.Status = CommissionPayoutStatus.Cancelled;
payout.ProcessedBy = _currentUser.GetPerformedBy();
payout.ProcessedAt = DateTime.UtcNow;
payout.RejectionReason = request.Reason;
payout.LastModified = DateTime.UtcNow;
// TODO: Add PayoutHistory record with rejection reason

View File

@@ -3,10 +3,14 @@ namespace CMSMicroservice.Application.CommissionCQ.Commands.RequestWithdrawal;
public class RequestWithdrawalCommandHandler : IRequestHandler<RequestWithdrawalCommand, Unit>
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
public RequestWithdrawalCommandHandler(IApplicationDbContext context)
public RequestWithdrawalCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<Unit> Handle(RequestWithdrawalCommand request, CancellationToken cancellationToken)