Files
CMS/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs
masoodafar-web aba534e07c
All checks were successful
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 2m12s
fix: update week calculation to use Saturday as the start of the week
2025-12-12 04:37:34 +03:30

355 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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<NetworkWeeklyBalance>();
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;
}
/// <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 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);
}
/// <summary>
/// محاسبه مجموع تعادل‌های زیرمجموعه یک کاربر تا maxLevel لول پایین‌تر
/// </summary>
private async Task<int> CalculateSubordinateBalancesAsync(
long userId,
Dictionary<long, NetworkWeeklyBalance> 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;
}
/// <summary>
/// پیدا کردن بازگشتی زیرمجموعه‌ها تا maxLevel لول
/// </summary>
private async Task<List<long>> GetSubordinatesRecursive(
long userId,
int currentLevel,
int maxLevel,
CancellationToken cancellationToken)
{
// محدودیت عمق
if (currentLevel > maxLevel)
{
return new List<long>();
}
var result = new List<long>();
// پیدا کردن فرزندان مستقیم
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;
}
}