namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances; public class CalculateWeeklyBalancesCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; public CalculateWeeklyBalancesCommandHandler(IApplicationDbContext context) { _context = context; } public async Task 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(); 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; } /// /// شماره هفته قبل را محاسبه می‌کند /// 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}"; } /// /// شمارش اعضای جدیدی که در این هفته به یک پا اضافه شدند /// تا maxLevel لول پایین‌تر شمارش می‌شود /// private async Task 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; } /// /// شمارش بازگشتی اعضای جدید در یک پا /// محدودیت عمق: تا maxLevel لول پایین‌تر شمارش می‌شود /// private async Task 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; } /// /// تبدیل شماره هفته به بازه تاریخی /// 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); } }