using System.Collections.Generic; using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Domain.Common; using CMSMicroservice.Domain.Entities.Club; using CMSMicroservice.Domain.Entities.Commission; using CMSMicroservice.Domain.Entities.Network; using CMSMicroservice.Domain.Enums; using Microsoft.EntityFrameworkCore; namespace CMSMicroservice.Infrastructure.Services.Commission; /// /// پیاده‌سازی محاسبه کمیسیون با استفاده از Entity Framework (ORM) /// این روش برای داده‌های کم مناسب است و دیباگ و تست راحت‌تری دارد /// public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy { private readonly IApplicationDbContext _context; private readonly IWeekDefinitionRepository _weekRepository; public OrmCommissionCalculationStrategy( IApplicationDbContext context, IWeekDefinitionRepository weekRepository) { _context = context; _weekRepository = weekRepository; } /// public async Task CalculateWeeklyBalancesAsync( long weekDefinitionId, bool forceRecalculate, CancellationToken cancellationToken = default) { var regorianWeekNumber = _weekRepository.GetGregorianWeekNumber(weekDefinitionId); if (regorianWeekNumber == null) { throw new InvalidOperationException($"هفته {weekDefinitionId} در سیستم تعریف نشده است"); } // بررسی وجود محاسبه قبلی var existingBalances = await _context.NetworkWeeklyBalances .Where(x => x.WeekDefinitionId == weekDefinitionId) .ToListAsync(cancellationToken); if (existingBalances.Any() && !forceRecalculate) { throw new InvalidOperationException($"تعادل‌های هفته {weekDefinitionId} قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید"); } // حذف محاسبات قبلی در صورت ForceRecalculate if (existingBalances.Any()) { _context.NetworkWeeklyBalances.RemoveRange(existingBalances); await _context.SaveChangesAsync(cancellationToken); } // دریافت همه کاربرانی که عضو فعال باشگاه هستند // ⚠️ Magic Wallet: کاربرهایی که در حالت جادویی هستند از کمیسیون خارج میشن var magicModeUserIds = await _context.UserWallets .Where(w => w.WalletMode == WalletMode.Magic) .Select(w => w.UserId) .ToHashSetAsync(cancellationToken); var activeClubMemberUserIds = await _context.ClubMemberships .Where(c => c.IsActive && !magicModeUserIds.Contains(c.UserId)) .Select(c => c.UserId) .ToHashSetAsync(cancellationToken); // دریافت کاربران فعال در شبکه که عضو باشگاه هستند var usersInNetwork = await _context.Users .Where(x => activeClubMemberUserIds.Contains(x.Id)) .Select(x => new { x.Id }) .ToListAsync(cancellationToken); // دریافت باقیمانده‌های هفته قبل var previousWeekDefinitionId = GetPreviousWeekDefinitionId(weekDefinitionId); Dictionary previousWeekCarryovers; if (previousWeekDefinitionId.HasValue) { previousWeekCarryovers = await _context.NetworkWeeklyBalances .Where(x => x.WeekDefinitionId == previousWeekDefinitionId.Value) .ToDictionaryAsync( x => x.UserId, x => (x.LeftLegRemainder, x.RightLegRemainder), cancellationToken); } else { previousWeekCarryovers = new Dictionary(); } var balancesList = new List(); var calculatedAt = DateTime.Now; // استفاده از SystemConstants به جای دیتابیس var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg; var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel; 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, weekDefinitionId, maxNetworkLevel, cancellationToken); var rightNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Right, weekDefinitionId, maxNetworkLevel, cancellationToken); // محاسبه مجموع هر پا (جدید + باقیمانده) var leftTotal = leftNewMembers + leftCarryover; var rightTotal = rightNewMembers + rightCarryover; // تعادل = کمترین مقدار بین چپ و راست var totalBalances = Math.Min(leftTotal, rightTotal); // باقیمانده برای هفته بعد var leftRemainder = leftTotal - totalBalances; var rightRemainder = rightTotal - totalBalances; // اعمال سقف 300 var cappedBalances = Math.Min(totalBalances, maxBalancesPerLeg); // محاسبه فلش var flushedPerSide = totalBalances - cappedBalances; var totalFlushed = flushedPerSide * 2; var balance = new NetworkWeeklyBalance { UserId = user.Id, WeekDefinitionId = weekDefinitionId, LeftLegNewMembers = leftNewMembers, RightLegNewMembers = rightNewMembers, LeftLegCarryover = leftCarryover, RightLegCarryover = rightCarryover, LeftLegTotal = leftTotal, RightLegTotal = rightTotal, TotalBalances = cappedBalances, LeftLegRemainder = leftRemainder, RightLegRemainder = rightRemainder, FlushedPerSide = flushedPerSide, TotalFlushed = totalFlushed, SubordinateBalances = 0, #pragma warning disable CS0618 LeftLegBalances = leftTotal, RightLegBalances = rightTotal, #pragma warning restore CS0618 WeeklyPoolContribution = 0, CalculatedAt = calculatedAt, IsExpired = false }; balancesList.Add(balance); } await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken); await _context.SaveChangesAsync(cancellationToken); // محاسبه تعادل زیرمجموعه 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; } /// public async Task CalculateWeeklyCommissionPoolAsync( long weekDefinitionId, bool forceRecalculate, CancellationToken cancellationToken = default) { var weekDefinition = _weekRepository.GetGregorianWeekNumber(weekDefinitionId); if (weekDefinition == null) { throw new InvalidOperationException($"هفته {weekDefinitionId} در سیستم تعریف نشده است"); } // بررسی وجود استخر var existingPool = await _context.WeeklyCommissionPools .FirstOrDefaultAsync(x => x.WeekDefinitionId == weekDefinitionId, cancellationToken); if (existingPool == null) { throw new InvalidOperationException( $"Pool هفته {weekDefinitionId} وجود ندارد. " + "Pool باید در هنگام فعالسازی باشگاه مشتریان ایجاد شده باشد" ); } if (existingPool.IsCalculated && !forceRecalculate) { throw new InvalidOperationException($"استخر کمیسیون هفته {weekDefinitionId} قبلاً محاسبه شده است"); } // بررسی وجود تعادل‌های هفتگی var weeklyBalances = await _context.NetworkWeeklyBalances .Where(x => x.WeekDefinitionId == weekDefinitionId) .ToListAsync(cancellationToken); if (!weeklyBalances.Any()) { throw new InvalidOperationException($"تعادل‌های هفته {weekDefinitionId} هنوز محاسبه نشده است. ابتدا CalculateWeeklyBalances را اجرا کنید"); } var totalPoolAmount = existingPool.TotalPoolAmount; var totalBalancesInNetwork = weeklyBalances.Sum(x => x.TotalBalances); long valuePerBalance = 0; if (totalBalancesInNetwork > 0) { valuePerBalance = totalPoolAmount / totalBalancesInNetwork; } // به‌روزرسانی Pool existingPool.TotalBalances = totalBalancesInNetwork; existingPool.ValuePerBalance = valuePerBalance; existingPool.IsCalculated = true; existingPool.CalculatedAt = DateTime.Now; _context.WeeklyCommissionPools.Update(existingPool); await _context.SaveChangesAsync(cancellationToken); // حذف پرداخت‌های قبلی در صورت ForceRecalculate if (forceRecalculate) { var oldPayouts = await _context.UserCommissionPayouts .Where(p => p.WeekDefinitionId == weekDefinitionId) .ToListAsync(cancellationToken); if (oldPayouts.Any()) { var oldPayoutIds = oldPayouts.Select(p => p.Id).ToList(); // برگرداندن NetworkBalance به حالت قبل (Revert) await RevertUserWalletsAsync(oldPayoutIds, cancellationToken); // حذف تاریخچه‌ها var oldHistories = await _context.CommissionPayoutHistories .Where(h => oldPayoutIds.Contains(h.UserCommissionPayoutId)) .ToListAsync(cancellationToken); if (oldHistories.Any()) { _context.CommissionPayoutHistories.RemoveRange(oldHistories); } _context.UserCommissionPayouts.RemoveRange(oldPayouts); await _context.SaveChangesAsync(cancellationToken); } } // ثبت پرداخت برای کاربرانی که تعادل دارند var payouts = new List(); foreach (var balance in weeklyBalances) { var userBalance = balance.TotalBalances; if (userBalance <= 0) continue; var totalAmount = (long)(userBalance * valuePerBalance); var payout = new UserCommissionPayout { UserId = balance.UserId, WeekDefinitionId = weekDefinitionId, WeeklyPoolId = existingPool.Id, BalancesEarned = userBalance, ValuePerBalance = valuePerBalance, TotalAmount = totalAmount, Status = CommissionPayoutStatus.Pending, PaidAt = null, WithdrawalMethod = null, IbanNumber = null, WithdrawnAt = null }; payouts.Add(payout); } if (payouts.Any()) { await _context.UserCommissionPayouts.AddRangeAsync(payouts, cancellationToken); await _context.SaveChangesAsync(cancellationToken); // ثبت تاریخچه var historyList = payouts.Select(payout => new CommissionPayoutHistory { UserCommissionPayoutId = payout.Id, UserId = payout.UserId, WeekDefinitionId = weekDefinitionId, AmountBefore = 0, AmountAfter = payout.TotalAmount, OldStatus = default(CommissionPayoutStatus), NewStatus = CommissionPayoutStatus.Pending, Action = CommissionPayoutAction.Created, PerformedBy = "System", Reason = "پردازش خودکار کمیسیون هفتگی (ORM)" }).ToList(); await _context.CommissionPayoutHistories.AddRangeAsync(historyList, cancellationToken); await _context.SaveChangesAsync(cancellationToken); // شارژ کیف پول کاربران await ChargeUserWalletsAsync(payouts, weekDefinitionId, cancellationToken); } return existingPool.Id; } #region Private Methods private long? GetPreviousWeekDefinitionId(long currentWeekDefinitionId) { var currentWeekNumber = _weekRepository.GetGregorianWeekNumber(currentWeekDefinitionId); if (currentWeekNumber == null) return null; var previousWeekNumber = _weekRepository.GetPreviousWeekNumber(currentWeekNumber); if (previousWeekNumber == null) return null; return _weekRepository.GetWeekDefinitionId(previousWeekNumber); } private async Task CountNewMembersInLeg(long userId, NetworkLeg leg, long weekDefinitionId, int maxLevel, CancellationToken cancellationToken) { var startDateEndDate = _weekRepository.GetWeekDateRange(weekDefinitionId); var count = await CountNewMembersRecursive(userId, leg, startDateEndDate.Value.startDate, startDateEndDate.Value.endDate, 0, maxLevel, cancellationToken); return count; } 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; // ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد) var currentCycle = await _context.ClubMembershipCycles .FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsCurrentCycle, cancellationToken); if (currentCycle?.PackagePurchasedAt >= startDate && currentCycle?.PackagePurchasedAt <= 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 async Task CalculateSubordinateBalancesAsync( long userId, Dictionary allBalances, int maxLevel, CancellationToken cancellationToken) { 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; } 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; } private async Task ChargeUserWalletsAsync( List payouts, long weekDefinitionId, CancellationToken cancellationToken) { var userIds = payouts.Select(p => p.UserId).Distinct().ToList(); var existingWallets = await _context.UserWallets .Where(w => userIds.Contains(w.UserId)) .ToDictionaryAsync(w => w.UserId, cancellationToken); var newWallets = new List(); var walletLogs = new List(); foreach (var payout in payouts) { if (payout.TotalAmount <= 0) continue; UserWallet wallet; if (existingWallets.TryGetValue(payout.UserId, out var existingWallet)) { wallet = existingWallet; } else { wallet = new UserWallet { UserId = payout.UserId, Balance = 0, NetworkBalance = 0, DiscountBalance = 0 }; newWallets.Add(wallet); existingWallets[payout.UserId] = wallet; } } if (newWallets.Any()) { await _context.UserWallets.AddRangeAsync(newWallets, cancellationToken); await _context.SaveChangesAsync(cancellationToken); } foreach (var payout in payouts) { if (payout.TotalAmount <= 0) continue; var wallet = existingWallets[payout.UserId]; wallet.NetworkBalance += payout.TotalAmount; var walletLog = new UserWalletChangeLog { WalletId = wallet.Id, CurrentBalance = wallet.Balance, ChangeValue = 0, CurrentNetworkBalance = wallet.NetworkBalance, ChangeNerworkValue = payout.TotalAmount, CurrentDiscountBalance = wallet.DiscountBalance, ChangeDiscountValue = 0, IsIncrease = true, RefrenceId = payout.Id }; walletLogs.Add(walletLog); } await _context.UserWalletChangeLogs.AddRangeAsync(walletLogs, cancellationToken); await _context.SaveChangesAsync(cancellationToken); } private async Task RevertUserWalletsAsync( List oldPayoutIds, CancellationToken cancellationToken) { var oldWalletLogs = await _context.UserWalletChangeLogs .Where(l => l.RefrenceId.HasValue && oldPayoutIds.Contains(l.RefrenceId.Value)) .ToListAsync(cancellationToken); if (!oldWalletLogs.Any()) return; var walletIds = oldWalletLogs.Select(l => l.WalletId).Distinct().ToList(); var wallets = await _context.UserWallets .Where(w => walletIds.Contains(w.Id)) .ToDictionaryAsync(w => w.Id, cancellationToken); foreach (var log in oldWalletLogs) { if (wallets.TryGetValue(log.WalletId, out var wallet)) { wallet.NetworkBalance -= log.ChangeNerworkValue; if (wallet.NetworkBalance < 0) { wallet.NetworkBalance = 0; } } } _context.UserWalletChangeLogs.RemoveRange(oldWalletLogs); await _context.SaveChangesAsync(cancellationToken); } #endregion }