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 activeClubMemberUserIds = await _context.ClubMemberships .Where(c => c.IsActive) .Select(c => c.UserId) .ToHashSetAsync(cancellationToken); // دریافت کاربران فعال در شبکه که عضو باشگاه هستند // نکته: شرط NetworkParentId.HasValue نداریم چون ریشه شبکه (اولین نفر) هم باید حساب بشه var usersInNetwork = await _context.Users .Where(x => activeClubMemberUserIds.Contains(x.Id)) .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.Now; // خواندن یکباره 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.OrderBy(o=>o.Id)) { // دریافت باقیمانده هفته قبل 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; // ✅ مرحله 1: محاسبه تعادل اولیه (قبل از اعمال سقف) // تعادل = کمترین مقدار بین چپ و راست // مثال: چپ=500، راست=600 → تعادل=500 var totalBalances = Math.Min(leftTotal, rightTotal); // ✅ مرحله 2: محاسبه باقیمانده (قبل از سقف) // باقیمانده = اضافه‌ای که یک طرف دارد // مثال: چپ=500، راست=600، تعادل=500 // → باقی چپ = 500-500 = 0 // → باقی راست = 600-500 = 100 (می‌رود برای هفته بعد) var leftRemainder = leftTotal - totalBalances; var rightRemainder = rightTotal - totalBalances; // ✅ مرحله 3: اعمال سقف 300 (برای امتیاز نهایی) // از تعادل، فقط 300 از هر طرف حساب می‌شود // مثال: تعادل=500 → امتیاز=300 // از چپ: 300 حساب می‌شود، 200 فلش می‌شود // از راست: 300 حساب می‌شود، 200 فلش می‌شود // جمع فلش = 400 (از بین می‌رود) var cappedBalances = Math.Min(totalBalances, maxBalancesPerLeg); // ✅ مرحله 4: محاسبه فلش (از هر دو طرف) var flushedPerSide = totalBalances - cappedBalances; // 500-300=200 var totalFlushed = flushedPerSide * 2; // 200×2=400 (از بین می‌رود) // ⚠️ توجه: تعادل زیرمجموعه در این مرحله محاسبه نمیشه // چون هنوز تمام تعادل‌ها محاسبه نشدن // بعد از ذخیره همه تعادل‌ها، در یک حلقه دوم محاسبه خواهد شد var balance = new NetworkWeeklyBalance { UserId = user.Id, WeekNumber = request.WeekNumber, // اطلاعات جدید LeftLegNewMembers = leftNewMembers, RightLegNewMembers = rightNewMembers, LeftLegCarryover = leftCarryover, RightLegCarryover = rightCarryover, // مجموع LeftLegTotal = leftTotal, RightLegTotal = rightTotal, TotalBalances = cappedBalances, // امتیاز نهایی بعد از اعمال سقف 300 // باقیمانده برای هفته بعد (اضافه‌ای که یک طرف دارد) LeftLegRemainder = leftRemainder, RightLegRemainder = rightRemainder, // فلش (از دست رفته) FlushedPerSide = flushedPerSide, TotalFlushed = totalFlushed, // تعادل زیرمجموعه - فعلاً 0 (بعد از ذخیره همه تعادل‌ها محاسبه میشه) SubordinateBalances = 0, // فیلدهای قدیمی (deprecated) - برای سازگاری با کدهای قبلی #pragma warning disable CS0618 LeftLegBalances = leftTotal, RightLegBalances = rightTotal, #pragma warning restore CS0618 WeeklyPoolContribution = 0, // Pool در مرحله بعد محاسبه میشه CalculatedAt = calculatedAt, IsExpired = false }; balancesList.Add(balance); } await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken); await _context.SaveChangesAsync(cancellationToken); // ⭐ مرحله 2: محاسبه تعادل زیرمجموعه برای هر کاربر (تا 15 لول) // حالا که همه تعادل‌ها ذخیره شدن، می‌تونیم تعادل زیرمجموعه رو حساب کنیم var balancesDictionary = balancesList.ToDictionary(x => x.UserId); foreach (var balance in balancesList) { var subordinateBalances = await CalculateSubordinateBalancesAsync( balance.UserId, balancesDictionary, maxNetworkLevel, cancellationToken ); balance.SubordinateBalances = subordinateBalances; } // ذخیره تعادل‌های زیرمجموعه _context.NetworkWeeklyBalances.UpdateRange(balancesList); 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 jan1DayOfWeek = (int)jan1.DayOfWeek; // اگر 1 ژانویه شنبه باشد: offset=0، اگر یکشنبه: offset=6، دوشنبه: offset=5، ... var daysToFirstSaturday = jan1DayOfWeek == 6 ? 0 : (6 - jan1DayOfWeek + 7) % 7; var firstSaturday = jan1.AddDays(daysToFirstSaturday); var weekStart = firstSaturday.AddDays((week - 1) * 7); var weekEnd = weekStart.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59); return (weekStart, weekEnd); } /// /// محاسبه مجموع تعادل‌های زیرمجموعه یک کاربر تا maxLevel لول پایین‌تر /// private async Task CalculateSubordinateBalancesAsync( long userId, Dictionary allBalances, int maxLevel, CancellationToken cancellationToken) { // پیدا کردن همه زیرمجموعه‌ها تا maxLevel لول var subordinates = await GetSubordinatesRecursive(userId, 1, maxLevel, cancellationToken); // جمع تعادل‌های آنها var totalSubordinateBalances = 0; foreach (var subordinateId in subordinates) { if (allBalances.ContainsKey(subordinateId)) { totalSubordinateBalances += allBalances[subordinateId].TotalBalances; } } return totalSubordinateBalances; } /// /// پیدا کردن بازگشتی زیرمجموعه‌ها تا maxLevel لول /// private async Task> GetSubordinatesRecursive( long userId, int currentLevel, int maxLevel, CancellationToken cancellationToken) { // محدودیت عمق if (currentLevel > maxLevel) { return new List(); } var result = new List(); // پیدا کردن فرزندان مستقیم var children = await _context.Users .Where(x => x.NetworkParentId == userId) .Select(x => x.Id) .ToListAsync(cancellationToken); result.AddRange(children); // بازگشت برای هر فرزند foreach (var childId in children) { var grandChildren = await GetSubordinatesRecursive(childId, currentLevel + 1, maxLevel, cancellationToken); result.AddRange(grandChildren); } return result; } }