252 lines
11 KiB
C#
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);
|
|
}
|
|
}
|