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:
masoodafar-web
2026-02-26 00:57:20 +03:30
parent 0002a5a6f2
commit 607f791b65
8 changed files with 144 additions and 63 deletions
@@ -8,6 +8,9 @@
CREATE OR ALTER PROCEDURE [CMS].[sp_CalculateWeeklyBalances]
@WeekDefinitionId BIGINT,
@ForceRecalculate BIT = 0,
@PackageId BIGINT = NULL, -- پکیج خاص (اگر NULL از پکیج پایه استفاده می‌کند)
@InputMaxBalancesPerLeg INT = NULL, -- سقف تعادل هر پا (از Package entity)
@InputMaxNetworkLevel INT = NULL, -- حداکثر عمق شبکه (از Package entity)
@RowCount INT OUTPUT
AS
BEGIN
@@ -18,10 +21,33 @@ BEGIN
DECLARE @StartDate DATETIME;
DECLARE @EndDate DATETIME;
DECLARE @PreviousWeekDefinitionId BIGINT;
DECLARE @MaxBalancesPerLeg INT = 300;
DECLARE @MaxNetworkLevel INT = 15;
DECLARE @MaxBalancesPerLeg INT;
DECLARE @MaxNetworkLevel INT;
DECLARE @CalculatedAt DATETIME = GETDATE();
-- تعیین PackageId پیش‌فرض (پکیج پایه)
IF @PackageId IS NULL
BEGIN
SELECT TOP 1 @PackageId = Id
FROM CMS.Packages
WHERE IsBasePackage = 1 AND IsDeleted = 0;
END
-- خواندن تنظیمات از Package entity اگر پارامتر داده نشده
IF @InputMaxBalancesPerLeg IS NOT NULL
SET @MaxBalancesPerLeg = @InputMaxBalancesPerLeg;
ELSE
SELECT @MaxBalancesPerLeg = ISNULL(MaxBalancesPerLeg, 300) FROM CMS.Packages WHERE Id = @PackageId;
IF @InputMaxNetworkLevel IS NOT NULL
SET @MaxNetworkLevel = @InputMaxNetworkLevel;
ELSE
SELECT @MaxNetworkLevel = ISNULL(MaxNetworkLevel, 15) FROM CMS.Packages WHERE Id = @PackageId;
-- fallbackهای امن
SET @MaxBalancesPerLeg = ISNULL(@MaxBalancesPerLeg, 300);
SET @MaxNetworkLevel = ISNULL(@MaxNetworkLevel, 15);
BEGIN TRY
BEGIN TRANSACTION;
@@ -65,11 +91,9 @@ BEGIN
AND IsActive = 1;
-- =============================================
-- 4. مقادیر ثابت (Hardcoded - از SystemConstants)
-- 4. تنظیمات پکیج (از پارامترها خوانده می‌شود)
-- =============================================
-- این مقادیر ثابت هستند و تغییر نمی‌کنند
SET @MaxBalancesPerLeg = 300; -- سقف تعادل هر پا
SET @MaxNetworkLevel = 15; -- حداکثر عمق شبکه
-- @MaxBalancesPerLeg و @MaxNetworkLevel قبلاً مقداردهی شده‌اند
-- =============================================
-- 5. ایجاد جدول موقت برای نتایج
@@ -96,7 +120,8 @@ BEGIN
INSERT INTO #Balances (UserId)
SELECT DISTINCT u.Id
FROM CMS.Users u
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = u.Id AND cm.IsActive = 1;
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = u.Id AND cm.IsActive = 1
WHERE cm.PackageId = @PackageId;
-- =============================================
-- 7. دریافت باقیمانده هفته قبل
@@ -107,7 +132,9 @@ BEGIN
SET b.LeftLegCarryover = ISNULL(nb.LeftLegRemainder, 0),
b.RightLegCarryover = ISNULL(nb.RightLegRemainder, 0)
FROM #Balances b
LEFT JOIN CMS.NetworkWeeklyBalances nb ON nb.UserId = b.UserId AND nb.WeekDefinitionId = @PreviousWeekDefinitionId;
LEFT JOIN CMS.NetworkWeeklyBalances nb ON nb.UserId = b.UserId
AND nb.WeekDefinitionId = @PreviousWeekDefinitionId
AND nb.PackageId = @PackageId;
END
-- =============================================
@@ -269,6 +296,7 @@ BEGIN
INSERT INTO CMS.NetworkWeeklyBalances (
UserId,
WeekDefinitionId,
PackageId,
LeftLegNewMembers,
RightLegNewMembers,
LeftLegCarryover,
@@ -295,6 +323,7 @@ BEGIN
SELECT
UserId,
@WeekDefinitionId,
@PackageId,
LeftLegNewMembers,
RightLegNewMembers,
LeftLegCarryover,
@@ -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,
@@ -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
{