Files
CMS/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs
T

252 lines
11 KiB
C#

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 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.MaxWeeklyBalancesPerLeg" ||
x.Key == "Commission.MaxNetworkLevel"))
.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;
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300"));
// حداکثر عمق شبکه برای شمارش اعضا (15 لول)
var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15"));
foreach (var user in usersInNetwork)
{
// دریافت باقیمانده هفته قبل
var leftCarryover = 0;
var rightCarryover = 0;
if (previousWeekCarryovers.ContainsKey(user.Id))
{
leftCarryover = previousWeekCarryovers[user.Id].LeftLegRemainder;
rightCarryover = previousWeekCarryovers[user.Id].RightLegRemainder;
}
// محاسبه تعداد اعضای جدید در این هفته (تا maxNetworkLevel لول پایین‌تر)
var leftNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Left, request.WeekNumber, maxNetworkLevel, cancellationToken);
var rightNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Right, request.WeekNumber, maxNetworkLevel, cancellationToken);
// محاسبه مجموع هر پا (جدید + باقیمانده)
var leftTotal = leftNewMembers + leftCarryover;
var rightTotal = rightNewMembers + rightCarryover;
// ✅ اصلاح شده: اعمال سقف روی هر دست جداگانه (نه روی کل)
// سقف 300 برای دست چپ + 300 برای دست راست = حداکثر 600 تعادل در هفته
var cappedLeftTotal = Math.Min(leftTotal, maxBalancesPerLeg);
var cappedRightTotal = Math.Min(rightTotal, maxBalancesPerLeg);
// محاسبه تعادل (کمترین مقدار بعد از اعمال سقف)
var totalBalances = Math.Min(cappedLeftTotal, cappedRightTotal);
// محاسبه باقیمانده برای هفته بعد
// باقیمانده = مقداری که از سقف هر دست رد شده
// مثال: چپ=350، راست=450، سقف=300
// cappedLeft = MIN(350, 300) = 300
// cappedRight = MIN(450, 300) = 300
// totalBalances = MIN(300, 300) = 300
// leftRemainder = 350 - 300 = 50 (مازاد سقف)
// rightRemainder = 450 - 300 = 150 (مازاد سقف)
var leftRemainder = leftTotal - cappedLeftTotal;
var rightRemainder = rightTotal - cappedRightTotal;
// محاسبه سهم استخر (20% از مجموع فعال‌سازی‌های جدید کل شبکه)
// طبق گفته دکتر: کل افراد جدید در شبکه × هزینه فعال‌سازی × 20%
var totalNewMembers = leftNewMembers + rightNewMembers;
var weeklyPoolContribution = (long)(totalNewMembers * activationFee * poolPercent);
var balance = new NetworkWeeklyBalance
{
UserId = user.Id,
WeekNumber = request.WeekNumber,
// اطلاعات جدید
LeftLegNewMembers = leftNewMembers,
RightLegNewMembers = rightNewMembers,
LeftLegCarryover = leftCarryover,
RightLegCarryover = rightCarryover,
// مجموع
LeftLegTotal = leftTotal,
RightLegTotal = rightTotal,
TotalBalances = totalBalances, // تعادل واقعی بعد از اعمال سقف روی هر دست
// باقیمانده برای هفته بعد (مازاد سقف هر دست)
LeftLegRemainder = leftRemainder,
RightLegRemainder = rightRemainder,
// فیلدهای قدیمی (deprecated) - برای سازگاری با کدهای قبلی
#pragma warning disable CS0618
LeftLegBalances = leftTotal,
RightLegBalances = rightTotal,
#pragma warning restore CS0618
WeeklyPoolContribution = weeklyPoolContribution,
CalculatedAt = calculatedAt,
IsExpired = false
};
balancesList.Add(balance);
}
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return balancesList.Count;
}
/// <summary>
/// شماره هفته قبل را محاسبه می‌کند
/// </summary>
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>
/// شمارش اعضای جدیدی که در این هفته به یک پا اضافه شدند
/// تا maxLevel لول پایین‌تر شمارش می‌شود
/// </summary>
private async Task<int> CountNewMembersInLeg(long userId, NetworkLeg leg, string weekNumber, int maxLevel, CancellationToken cancellationToken)
{
// تبدیل WeekNumber به بازه تاریخی
var (startDate, endDate) = GetWeekDateRange(weekNumber);
// شمارش تمام اعضای زیرمجموعه که در این هفته فعال شدند (تا maxLevel لول)
var count = await CountNewMembersRecursive(userId, leg, startDate, endDate, 0, maxLevel, cancellationToken);
return count;
}
/// <summary>
/// شمارش بازگشتی اعضای جدید در یک پا
/// محدودیت عمق: تا maxLevel لول پایین‌تر شمارش می‌شود
/// </summary>
private async Task<int> CountNewMembersRecursive(
long userId,
NetworkLeg leg,
DateTime startDate,
DateTime endDate,
int currentLevel,
int maxLevel,
CancellationToken cancellationToken)
{
// ⭐ محدودیت عمق: اگر به حداکثر لول رسیدیم، توقف
if (currentLevel >= maxLevel)
{
return 0;
}
// پیدا کردن فرزند مستقیم در پای مورد نظر
var child = await _context.Users
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == leg, cancellationToken);
if (child == null)
{
return 0;
}
var count = 0;
// اگر فرزند در این هفته فعال شده، 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, currentLevel + 1, maxLevel, cancellationToken);
var childRight = await CountNewMembersRecursive(child.Id, NetworkLeg.Right, startDate, endDate, currentLevel + 1, maxLevel, 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);
}
}