Phase 5: Per-package commission + golden ref cleanup
- Fix golden/طلایی string refs in user-facing messages (ActivateClubMembership) - Rename HasPurchasedGoldenPackage → HasPurchasedPackage (DTO, Handler, Proto, Mapping) - ORM commission: per-user-package calculation via ClubMembership.LastPackageId - Build userPackageMap, per-user maxBalancesPerLeg/maxNetworkLevel - Carryover keyed by (UserId, PackageId) tuple - SP commission: loop over packages, pass @PackageId/@InputMaxBalancesPerLeg/@InputMaxNetworkLevel - sp_CalculateWeeklyBalances: accept 3 new params, filter by PackageId, insert PackageId column
This commit is contained in:
+40
-25
@@ -62,10 +62,20 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
|
||||
.Select(w => w.UserId)
|
||||
.ToHashSetAsync(cancellationToken);
|
||||
|
||||
var activeClubMemberUserIds = await _context.ClubMemberships
|
||||
// دریافت عضویتهای فعال به همراه PackageId هر کاربر
|
||||
var activeClubMemberships = await _context.ClubMemberships
|
||||
.Where(c => c.IsActive && !magicModeUserIds.Contains(c.UserId))
|
||||
.Select(c => new { c.UserId, PackageId = c.LastPackageId })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var activeClubMemberUserIds = activeClubMemberships
|
||||
.Select(c => c.UserId)
|
||||
.ToHashSetAsync(cancellationToken);
|
||||
.ToHashSet();
|
||||
|
||||
// نگاشت کاربر → PackageId
|
||||
var userPackageMap = activeClubMemberships
|
||||
.Where(c => c.PackageId.HasValue)
|
||||
.ToDictionary(c => c.UserId, c => c.PackageId!.Value);
|
||||
|
||||
// دریافت کاربران فعال در شبکه که عضو باشگاه هستند
|
||||
var usersInNetwork = await _context.Users
|
||||
@@ -73,43 +83,52 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
|
||||
.Select(x => new { x.Id })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// دریافت باقیماندههای هفته قبل
|
||||
// بارگذاری همه پکیجهای فعال (bulk load)
|
||||
var allPackages = await _context.Packages
|
||||
.Where(p => !p.IsDeleted)
|
||||
.ToDictionaryAsync(p => p.Id, cancellationToken);
|
||||
|
||||
// پکیج پیشفرض (fallback برای کاربرانی که PackageId ندارند)
|
||||
var defaultPackage = allPackages.Values
|
||||
.FirstOrDefault(p => p.IsBasePackage)
|
||||
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
|
||||
|
||||
// دریافت باقیماندههای هفته قبل — شامل PackageId
|
||||
var previousWeekDefinitionId = GetPreviousWeekDefinitionId(weekDefinitionId);
|
||||
Dictionary<long, (int LeftLegRemainder, int RightLegRemainder)> previousWeekCarryovers;
|
||||
Dictionary<(long UserId, long PackageId), (int LeftLegRemainder, int RightLegRemainder)> previousWeekCarryovers;
|
||||
|
||||
if (previousWeekDefinitionId.HasValue)
|
||||
{
|
||||
previousWeekCarryovers = await _context.NetworkWeeklyBalances
|
||||
.Where(x => x.WeekDefinitionId == previousWeekDefinitionId.Value)
|
||||
.ToDictionaryAsync(
|
||||
x => x.UserId,
|
||||
x => (x.UserId, x.PackageId),
|
||||
x => (x.LeftLegRemainder, x.RightLegRemainder),
|
||||
cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
previousWeekCarryovers = new Dictionary<long, (int, int)>();
|
||||
previousWeekCarryovers = new Dictionary<(long, long), (int, int)>();
|
||||
}
|
||||
|
||||
var balancesList = new List<NetworkWeeklyBalance>();
|
||||
var calculatedAt = DateTime.Now;
|
||||
|
||||
// بارگذاری پکیج پایه برای خواندن سقفها
|
||||
var package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
||||
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
|
||||
var maxBalancesPerLeg = package.MaxBalancesPerLeg;
|
||||
var maxNetworkLevel = package.MaxNetworkLevel;
|
||||
|
||||
foreach (var user in usersInNetwork.OrderBy(o => o.Id))
|
||||
{
|
||||
// دریافت باقیمانده هفته قبل
|
||||
// تعیین پکیج هر کاربر (per-user)
|
||||
var userPackageId = userPackageMap.GetValueOrDefault(user.Id, defaultPackage.Id);
|
||||
var package = allPackages.GetValueOrDefault(userPackageId, defaultPackage);
|
||||
var maxBalancesPerLeg = package.MaxBalancesPerLeg;
|
||||
var maxNetworkLevel = package.MaxNetworkLevel;
|
||||
// دریافت باقیمانده هفته قبل — فیلتر per-package
|
||||
var leftCarryover = 0;
|
||||
var rightCarryover = 0;
|
||||
if (previousWeekCarryovers.ContainsKey(user.Id))
|
||||
var carryoverKey = (UserId: user.Id, PackageId: userPackageId);
|
||||
if (previousWeekCarryovers.ContainsKey(carryoverKey))
|
||||
{
|
||||
leftCarryover = previousWeekCarryovers[user.Id].LeftLegRemainder;
|
||||
rightCarryover = previousWeekCarryovers[user.Id].RightLegRemainder;
|
||||
leftCarryover = previousWeekCarryovers[carryoverKey].LeftLegRemainder;
|
||||
rightCarryover = previousWeekCarryovers[carryoverKey].RightLegRemainder;
|
||||
}
|
||||
|
||||
// محاسبه تعداد اعضای جدید در این هفته (تا maxNetworkLevel لول پایینتر)
|
||||
@@ -166,15 +185,16 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
|
||||
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// محاسبه تعادل زیرمجموعه
|
||||
// محاسبه تعادل زیرمجموعه — per-user maxNetworkLevel
|
||||
var balancesDictionary = balancesList.ToDictionary(x => x.UserId);
|
||||
|
||||
foreach (var balance in balancesList)
|
||||
{
|
||||
var balancePackage = allPackages.GetValueOrDefault(balance.PackageId, defaultPackage);
|
||||
var subordinateBalances = await CalculateSubordinateBalancesAsync(
|
||||
balance.UserId,
|
||||
balancesDictionary,
|
||||
maxNetworkLevel,
|
||||
balancePackage.MaxNetworkLevel,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
@@ -193,11 +213,6 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// بارگذاری پکیج پایه برای تعیین PackageId روی پرداختها
|
||||
var package = await _context.Packages
|
||||
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
|
||||
?? throw new InvalidOperationException("پکیج پایه یافت نشد");
|
||||
|
||||
var weekDefinition = _weekRepository.GetGregorianWeekNumber(weekDefinitionId);
|
||||
if (weekDefinition == null)
|
||||
{
|
||||
@@ -293,7 +308,7 @@ public class OrmCommissionCalculationStrategy : ICommissionCalculationStrategy
|
||||
UserId = balance.UserId,
|
||||
WeekDefinitionId = weekDefinitionId,
|
||||
WeeklyPoolId = existingPool.Id,
|
||||
PackageId = package.Id,
|
||||
PackageId = balance.PackageId, // per-user PackageId از محاسبه تعادل
|
||||
BalancesEarned = userBalance,
|
||||
ValuePerBalance = valuePerBalance,
|
||||
TotalAmount = totalAmount,
|
||||
|
||||
+60
-23
@@ -6,6 +6,7 @@ namespace CMSMicroservice.Infrastructure.Services.Commission;
|
||||
/// <summary>
|
||||
/// پیادهسازی محاسبه کمیسیون با استفاده از Stored Procedure
|
||||
/// این روش برای دادههای زیاد بهینهتر است و از CTE استفاده میکند
|
||||
/// حلقه روی پکیجهای فعال — هر پکیج با تنظیمات خودش
|
||||
/// </summary>
|
||||
public class StoredProcedureCommissionCalculationStrategy : ICommissionCalculationStrategy
|
||||
{
|
||||
@@ -22,45 +23,81 @@ public class StoredProcedureCommissionCalculationStrategy : ICommissionCalculati
|
||||
bool forceRecalculate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// دسترسی به DbContext واقعی برای اجرای SP
|
||||
var dbContext = _context as DbContext;
|
||||
if (dbContext == null)
|
||||
{
|
||||
throw new InvalidOperationException("DbContext برای اجرای Stored Procedure در دسترس نیست");
|
||||
}
|
||||
|
||||
// بارگذاری پکیجهای فعال
|
||||
var activePackages = await _context.Packages
|
||||
.Where(p => !p.IsDeleted && p.IsActive)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (!activePackages.Any())
|
||||
{
|
||||
throw new InvalidOperationException("هیچ پکیج فعالی یافت نشد");
|
||||
}
|
||||
|
||||
var totalRowCount = 0;
|
||||
|
||||
var connection = dbContext.Database.GetDbConnection();
|
||||
await connection.OpenAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "CMS.sp_CalculateWeeklyBalances";
|
||||
command.CommandType = System.Data.CommandType.StoredProcedure;
|
||||
command.CommandTimeout = 300; // 5 دقیقه timeout
|
||||
// حلقه روی پکیجها — هر پکیج با تنظیمات خودش
|
||||
foreach (var package in activePackages)
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "CMS.sp_CalculateWeeklyBalances";
|
||||
command.CommandType = System.Data.CommandType.StoredProcedure;
|
||||
command.CommandTimeout = 300; // 5 دقیقه timeout
|
||||
|
||||
// پارامترها
|
||||
var paramWeekDefinitionId = command.CreateParameter();
|
||||
paramWeekDefinitionId.ParameterName = "@WeekDefinitionId";
|
||||
paramWeekDefinitionId.Value = weekDefinitionId;
|
||||
command.Parameters.Add(paramWeekDefinitionId);
|
||||
// پارامتر WeekDefinitionId
|
||||
var paramWeekDefinitionId = command.CreateParameter();
|
||||
paramWeekDefinitionId.ParameterName = "@WeekDefinitionId";
|
||||
paramWeekDefinitionId.Value = weekDefinitionId;
|
||||
command.Parameters.Add(paramWeekDefinitionId);
|
||||
|
||||
var paramForceRecalculate = command.CreateParameter();
|
||||
paramForceRecalculate.ParameterName = "@ForceRecalculate";
|
||||
paramForceRecalculate.Value = forceRecalculate;
|
||||
command.Parameters.Add(paramForceRecalculate);
|
||||
// پارامتر ForceRecalculate
|
||||
var paramForceRecalculate = command.CreateParameter();
|
||||
paramForceRecalculate.ParameterName = "@ForceRecalculate";
|
||||
paramForceRecalculate.Value = forceRecalculate;
|
||||
command.Parameters.Add(paramForceRecalculate);
|
||||
|
||||
// پارامتر خروجی
|
||||
var paramRowCount = command.CreateParameter();
|
||||
paramRowCount.ParameterName = "@RowCount";
|
||||
paramRowCount.Direction = System.Data.ParameterDirection.Output;
|
||||
paramRowCount.DbType = System.Data.DbType.Int32;
|
||||
command.Parameters.Add(paramRowCount);
|
||||
// پارامتر PackageId — per-package
|
||||
var paramPackageId = command.CreateParameter();
|
||||
paramPackageId.ParameterName = "@PackageId";
|
||||
paramPackageId.Value = package.Id;
|
||||
command.Parameters.Add(paramPackageId);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
// پارامتر MaxBalancesPerLeg — per-package
|
||||
var paramMaxBalances = command.CreateParameter();
|
||||
paramMaxBalances.ParameterName = "@InputMaxBalancesPerLeg";
|
||||
paramMaxBalances.Value = package.MaxBalancesPerLeg;
|
||||
command.Parameters.Add(paramMaxBalances);
|
||||
|
||||
var rowCount = (int)(paramRowCount.Value ?? 0);
|
||||
return rowCount;
|
||||
// پارامتر MaxNetworkLevel — per-package
|
||||
var paramMaxLevel = command.CreateParameter();
|
||||
paramMaxLevel.ParameterName = "@InputMaxNetworkLevel";
|
||||
paramMaxLevel.Value = package.MaxNetworkLevel;
|
||||
command.Parameters.Add(paramMaxLevel);
|
||||
|
||||
// پارامتر خروجی
|
||||
var paramRowCount = command.CreateParameter();
|
||||
paramRowCount.ParameterName = "@RowCount";
|
||||
paramRowCount.Direction = System.Data.ParameterDirection.Output;
|
||||
paramRowCount.DbType = System.Data.DbType.Int32;
|
||||
command.Parameters.Add(paramRowCount);
|
||||
|
||||
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||
|
||||
var rowCount = (int)(paramRowCount.Value ?? 0);
|
||||
totalRowCount += rowCount;
|
||||
}
|
||||
|
||||
return totalRowCount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user