feat: Enhance club membership activation and commission pool validation
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m9s

- Updated AcceptClubMembershipContractCommandHandler to set PackagePurchasedAt to the contract-signing time, ensuring accurate pool charging.
- Modified ActivateClubMembershipCommand to allow specifying a PackageId for forced activation, defaulting to the base package if not provided.
- Improved ActivateClubMembershipCommandHandler to handle new and existing memberships differently regarding pool charging.
- Added validation logic in GetWeeklyCommissionPoolQueryHandler to compare TotalPoolAmount against actual activations.
- Introduced validation fields in WeeklyCommissionPoolDto for tracking discrepancies and expected amounts.
- Updated stored procedures to ensure accurate calculations based on package purchases and to handle empty pools.
- Enhanced protobuf definitions to include validation fields for commission pools and network tree nodes.
- Added mapping for new fields in CommissionProfile and NetworkMembershipProfile.
- Created DiscountProductProfile for mapping discount product requests and responses.
This commit is contained in:
masoodafar-web
2026-05-01 00:07:05 +03:30
parent 789aa0b870
commit 8518c7b3ec
18 changed files with 423 additions and 115 deletions
@@ -221,7 +221,54 @@ public class AcceptClubMembershipContractCommandHandler
_context.ClubMembershipHistories.Add(history);
await _context.SaveChangesAsync(cancellationToken);
// 9. اضافه کردن مبلغ به Pool هفته جاری
// 8.5. آپدیت PackagePurchasedAt سایکل جاری به زمان امضای قرارداد
// دلیل: Pool در همین لحظه شارژ می‌شود — Validation باید به همین هفته اشاره کند
// سناریوی cross-week: کاربر هفته ۱۸ پکیج خریده، هفته ۱۹ قرارداد امضا کرده
// بدون این Fix: Cycle.PackagePurchasedAt = هفته ۱۸ ولی Pool هفته ۱۹ → اختلاف
// با این Fix: Cycle.PackagePurchasedAt = هفته ۱۹ = Pool هفته ۱۹ → درست
var currentCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(c => c.UserId == userId && c.IsCurrentCycle, cancellationToken);
if (currentCycle != null)
{
currentCycle.PackagePurchasedAt = activationDate;
_context.ClubMembershipCycles.Update(currentCycle);
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation(
"Updated Cycle #{CycleNumber} PackagePurchasedAt to contract-signing time for UserId {UserId}",
currentCycle.CycleNumber,
userId
);
}
else
{
// در صورتی که Cycle وجود ندارد (legacy یا مسیر مستقیم AcceptContract)
// Cycle را همین‌جا بساز تا Validation درست کار کند
_logger.LogWarning(
"No IsCurrentCycle found for UserId {UserId} at AcceptContract — creating Cycle now",
userId
);
var maxCycleNumber = await _context.ClubMembershipCycles
.Where(c => c.ClubMembershipId == clubMembership.Id)
.MaxAsync(c => (int?)c.CycleNumber, cancellationToken) ?? 0;
var newCycle = new ClubMembershipCycle
{
UserId = userId,
ClubMembershipId = clubMembership.Id,
CycleNumber = maxCycleNumber + 1,
PackagePurchasedAt = activationDate,
PurchaseMethod = user.PackagePurchaseMethod,
PackageAmount = package.Price,
PackageId = package.Id,
IsCurrentCycle = true
};
_context.ClubMembershipCycles.Add(newCycle);
await _context.SaveChangesAsync(cancellationToken);
}
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
var weeklyPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
@@ -17,4 +17,10 @@ public record ActivateClubMembershipCommand : IRequest<bool>
/// فعال‌سازی اجباری توسط ادمین — بدون بررسی سفارش و کیف‌پول
/// </summary>
public bool ForceActivation { get; init; }
/// <summary>
/// شناسه پکیج — فقط در ForceActivation استفاده می‌شود.
/// اگر 0 باشد، پکیج پایه (IsBasePackage) استفاده می‌شود.
/// </summary>
public long PackageId { get; init; }
}
@@ -170,10 +170,19 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
await _context.SaveChangesAsync(cancellationToken);
}
// بارگذاری پکیج پایه برای فعال‌سازی اجباری
package = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new NotFoundException("پکیج پایه یافت نشد");
// بارگذاری پکیج — اگر PackageId مشخص شده همان را لود کن، وگرنه پکیج پایه
if (request.PackageId > 0)
{
package = await _context.Packages
.FirstOrDefaultAsync(p => p.Id == request.PackageId && !p.IsDeleted, cancellationToken)
?? throw new NotFoundException($"پکیج {request.PackageId} یافت نشد");
}
else
{
package = await _context.Packages
.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken)
?? throw new NotFoundException("پکیج پایه یافت نشد");
}
}
// 6. بررسی عضویت فعلی
@@ -226,10 +235,11 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
var hasCurrentCycle = await _context.ClubMembershipCycles
.AnyAsync(c => c.UserId == user.Id && c.IsCurrentCycle, cancellationToken);
if (existingMembership.IsActive && hasCurrentCycle)
// اگر چرخه فعال دارد — یعنی Activate قبلاً برای این خرید اجرا شده؛ جلوگیری از ثبت Cycle تکراری
if (hasCurrentCycle)
{
_logger.LogInformation(
"User {UserId} is already an active club member with a running cycle",
"User {UserId} already has a running cycle — skipping duplicate activation",
user.Id
);
return true;
@@ -307,48 +317,60 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
await _context.SaveChangesAsync(cancellationToken);
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
var weeklyPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
if (weeklyPool == null)
// عضو جدید (isNewMembership=true): Pool توسط AcceptClubMembershipContract شارژ می‌شود
// چون کاربر قبل از امضای قرارداد IsActive=false است و قرارداد فقط یک‌بار امضا می‌شود.
// خرید مجدد (isNewMembership=false): قرارداد مجدد امضا نمی‌شود — Pool همین‌جا شارژ می‌شود.
if (!isNewMembership)
{
// ایجاد Pool جدید برای این هفته × این پکیج
weeklyPool = new WeeklyCommissionPool
{
WeekDefinitionId = currentWeekDefinitionId,
PackageId = package.Id,
TotalPoolAmount = activationFeeValue,
TotalBalances = 0, // در CalculateWeeklyBalances محاسبه میشه
ValuePerBalance = 0, // در CalculateWeeklyCommissionPool محاسبه میشه
IsCalculated = false,
CalculatedAt = null
};
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
var weeklyPool = await _context.WeeklyCommissionPools
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken);
await _context.WeeklyCommissionPools.AddAsync(weeklyPool, cancellationToken);
_logger.LogInformation(
"Created new WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId} with initial amount: {Amount}",
currentWeekDefinitionId,
giftValue
);
if (weeklyPool == null)
{
// ایجاد Pool جدید برای این هفته × این پکیج
weeklyPool = new WeeklyCommissionPool
{
WeekDefinitionId = currentWeekDefinitionId,
PackageId = package.Id,
TotalPoolAmount = activationFeeValue,
TotalBalances = 0,
ValuePerBalance = 0,
IsCalculated = false,
CalculatedAt = null
};
await _context.WeeklyCommissionPools.AddAsync(weeklyPool, cancellationToken);
_logger.LogInformation(
"Created new WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId} with initial amount: {Amount}",
currentWeekDefinitionId,
activationFeeValue
);
}
else
{
weeklyPool.TotalPoolAmount += activationFeeValue;
_context.WeeklyCommissionPools.Update(weeklyPool);
_logger.LogInformation(
"Added {Amount} to existing WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId}. New total: {NewTotal}",
activationFeeValue,
currentWeekDefinitionId,
weeklyPool.TotalPoolAmount
);
}
await _context.SaveChangesAsync(cancellationToken);
}
else
{
// اضافه کردن به Pool موجود
weeklyPool.TotalPoolAmount += activationFeeValue;
_context.WeeklyCommissionPools.Update(weeklyPool);
_logger.LogInformation(
"Added {Amount} to existing WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId}. New total: {NewTotal}",
activationFeeValue,
currentWeekDefinitionId,
weeklyPool.TotalPoolAmount
"New member — Pool charge deferred to AcceptClubMembershipContract for UserId {UserId}",
user.Id
);
}
await _context.SaveChangesAsync(cancellationToken);
// 9. اعمال ویژگی‌های پکیج — DIFF/تفاضل (Q20)
await ApplyFeatureDiffAsync(user.Id, entity.Id, package.Id, isNewMembership, activationDate, cancellationToken);
@@ -266,12 +266,16 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
var count = 0;
// اگر فرزند در این هفته پکیج خریده (Cycle جدید ساخته شده)، 1 امتیاز
// ⚠️ از ClubMembershipCycle.PackagePurchasedAt استفاده میکنیم (نه ActivatedAt که overwrite میشد)
var currentCycle = await _context.ClubMembershipCycles
.FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsCurrentCycle, cancellationToken);
// ⭐ بر اساس بازه تاریخی هفته، نه IsCurrentCycle
// IsCurrentCycle فقط آخرین خرید را نشان می‌دهد — برای تاریخچه‌ای (هفته‌های گذشته)
// باید از PackagePurchasedAt استفاده کرد که تاریخ دقیق خرید را نگه می‌دارد
var hadCycleInWeek = await _context.ClubMembershipCycles
.AnyAsync(x => x.UserId == child.Id
&& x.PackagePurchasedAt >= startDate
&& x.PackagePurchasedAt <= endDate,
cancellationToken);
if (currentCycle?.PackagePurchasedAt >= startDate && currentCycle?.PackagePurchasedAt <= endDate)
if (hadCycleInWeek)
{
count = 1;
}
@@ -41,6 +41,61 @@ public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler<GetWeeklyComm
})
.FirstOrDefaultAsync(cancellationToken);
return pool ?? new WeeklyCommissionPoolDto();
pool ??= new WeeklyCommissionPoolDto();
// ── Q29: اعتبارسنجی TotalPoolAmount در برابر فعال‌سازی‌های واقعی ──
await ValidatePoolAmountAsync(pool, request, cancellationToken);
return pool;
}
/// <summary>
/// TotalPoolAmount استخر را با تعداد فعال‌سازی‌های ثبت‌شده (AcceptContract + ActivateClubMembership) مقایسه می‌کند.
/// نتیجه در فیلدهای Validation... ذخیره می‌شود.
/// </summary>
private async Task ValidatePoolAmountAsync(WeeklyCommissionPoolDto pool, GetWeeklyCommissionPoolQuery request, CancellationToken cancellationToken)
{
var packageId = pool.PackageId > 0 ? pool.PackageId : request.PackageId;
if (packageId is null or 0)
return;
var weekDef = _weekDefinitionRepository.GetWeekById(request.WeekDefinitionId);
if (weekDef is null)
return;
var activationFee = await _context.Packages
.AsNoTracking()
.Where(p => p.Id == packageId.Value)
.Select(p => p.ActivationFee)
.FirstOrDefaultAsync(cancellationToken);
if (activationFee <= 0)
return;
// شمارش Cycle‌هایی که Pool آن‌ها واقعاً شارژ شده:
// - عضو جدید: Pool توسط AcceptContract شارژ می‌شود → UserContract وجود دارد
// - خرید مجدد: Pool در ActivateClubMembership شارژ می‌شود → IsCurrentCycle نبوده (قبلاً active بوده)
// - نباید از IsActive استفاده شود: وضعیت تاریخی نیست (عضو ممکن است بعداً غیرفعال شده باشد)
//
// قانون: Cycle شارژ شده = یا قرارداد دارد (عضو جدید) یا IsActive قبلاً true بوده (خرید مجدد)
// ساده‌ترین معادل: Cycle هایی که ClubMembership.ActivatedAt در بازه هفته است (زمان واقعی شارژ Pool)
var totalActivations = await _context.ClubMembershipCycles
.AsNoTracking()
.Where(c => c.PackageId == packageId.Value
&& c.PackagePurchasedAt >= weekDef.StartDate
&& c.PackagePurchasedAt < weekDef.EndDate
&& _context.UserContracts.Any(uc =>
uc.UserId == c.UserId
&& _context.Contracts.Any(con =>
con.Id == uc.ContractId
&& con.Type == ContractType.ClubMembership)))
.CountAsync(cancellationToken);
var expectedAmount = (long)totalActivations * activationFee;
pool.ValidationActivationCount = totalActivations;
pool.ValidationExpectedAmount = expectedAmount;
pool.ValidationDiscrepancy = pool.TotalPoolAmount - expectedAmount;
pool.ValidationIsValid = pool.TotalPoolAmount == expectedAmount;
}
}
@@ -16,4 +16,14 @@ public class WeeklyCommissionPoolDto
public DateTimeOffset Created { get; set; }
public long PackageId { get; set; }
public string PackageTitle { get; set; } = string.Empty;
// ── Validation Fields (Q29) ──
/// <summary>آیا TotalPoolAmount با تعداد فعال‌سازی‌های ثبت‌شده در دیتابیس مطابقت دارد؟</summary>
public bool ValidationIsValid { get; set; } = true;
/// <summary>مبلغ مورد انتظار بر اساس شمارش فعال‌سازی‌ها × ActivationFee</summary>
public long ValidationExpectedAmount { get; set; }
/// <summary>اختلاف: TotalPoolAmount - ValidationExpectedAmount</summary>
public long ValidationDiscrepancy { get; set; }
/// <summary>تعداد فعال‌سازی‌های شناسایی‌شده برای این هفته و پکیج</summary>
public int ValidationActivationCount { get; set; }
}
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
@@ -14,15 +15,18 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
{
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUser;
private readonly ISender _sender;
private readonly ILogger<CreateManualPaymentCommandHandler> _logger;
public CreateManualPaymentCommandHandler(
IApplicationDbContext context,
ICurrentUserService currentUser,
ISender sender,
ILogger<CreateManualPaymentCommandHandler> logger)
{
_context = context;
_currentUser = currentUser;
_sender = sender;
_logger = logger;
}
@@ -152,6 +156,29 @@ public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPay
// 10. ذخیره همه تغییرات
await _context.SaveChangesAsync(cancellationToken);
// 11. فعال‌سازی باشگاه مشتریان — ساخت Cycle و آماده‌سازی برای AcceptContract
// این مرحله تضمین می‌کند که:
// الف) ClubMembershipCycle ساخته شود تا والدین امتیاز «عضو جدید» بگیرند
// ب) Pool توسط AcceptContract (که کاربر در FO امضا می‌کند) شارژ شود
try
{
await _sender.Send(new ActivateClubMembershipCommand
{
UserId = request.UserId,
ForceActivation = true,
PackageId = package.Id
}, cancellationToken);
}
catch (Exception activateEx)
{
// عضویت باشگاه اختیاری است — خطا نباید پرداخت دستی را برگرداند
_logger.LogWarning(
activateEx,
"Club membership activation failed after manual payment for UserId {UserId} — payment recorded successfully",
request.UserId
);
}
_logger.LogInformation(
"Manual membership payment created successfully. " +
"ManualPaymentId: {Id}, UserId: {UserId}, TransactionId: {TransactionId}, " +
@@ -118,7 +118,10 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
ActivationWeekDefinitionId = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDefinitionId")) ? null : reader.GetInt64(reader.GetOrdinal("ActivationWeekDefinitionId")),
ActivationWeekDisplayName = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDisplayName")) ? null : reader.GetString(reader.GetOrdinal("ActivationWeekDisplayName")),
IsActivatedInTargetWeek = Convert.ToBoolean(reader.GetValue(reader.GetOrdinal("IsActivatedInTargetWeek"))),
UserCreated = new DateTimeOffset(reader.GetDateTime(reader.GetOrdinal("UserCreated")))
UserCreated = new DateTimeOffset(reader.GetDateTime(reader.GetOrdinal("UserCreated"))),
IsNewActivation = reader.IsDBNull(reader.GetOrdinal("IsNewActivation")) ? null : (bool?)(reader.GetInt32(reader.GetOrdinal("IsNewActivation")) == 1),
PackageName = reader.IsDBNull(reader.GetOrdinal("PackageName")) ? null : reader.GetString(reader.GetOrdinal("PackageName")),
PackageId = reader.IsDBNull(reader.GetOrdinal("PackageId")) ? null : reader.GetInt64(reader.GetOrdinal("PackageId"))
};
results.Add(node);
@@ -160,7 +163,10 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
ActivationWeekDefinitionId = flatNode.ActivationWeekDefinitionId,
ActivationWeekDisplayName = flatNode.ActivationWeekDisplayName,
IsActivatedInTargetWeek = flatNode.IsActivatedInTargetWeek,
UserCreated = flatNode.UserCreated
UserCreated = flatNode.UserCreated,
IsNewActivation = flatNode.IsNewActivation,
PackageName = flatNode.PackageName,
PackageId = flatNode.PackageId
};
}
@@ -47,7 +47,19 @@ public class NetworkTreeDto
/// تاریخ ایجاد کاربر
/// </summary>
public DateTimeOffset UserCreated { get; set; }
/// <summary>
/// آیا فعال‌سازی در هفته هدف اولین بار بوده؟ null = بدون Cycle
/// </summary>
public bool? IsNewActivation { get; set; }
/// <summary>
/// نام پکیج موثر (هفته هدف یا پکیج فعلی)
/// </summary>
public string? PackageName { get; set; }
public long? PackageId { get; set; }
public NetworkTreeDto? LeftChild { get; set; }
public NetworkTreeDto? RightChild { get; set; }
}
@@ -20,6 +20,19 @@ public class NetworkTreeNodeDto
public string? ActivationWeekDisplayName { get; set; }
public bool IsActivatedInTargetWeek { get; set; }
public DateTimeOffset UserCreated { get; set; }
/// <summary>
/// آیا فعال‌سازی در هفته هدف اولین بار بوده (true) یا خرید مجدد (false)؟
/// null یعنی Cycle‌ای ندارد
/// </summary>
public bool? IsNewActivation { get; set; }
/// <summary>
/// نام پکیج موثر (هفته هدف یا پکیج فعلی)
/// </summary>
public string? PackageName { get; set; }
public long? PackageId { get; set; }
/// <summary>
/// نام کامل کاربر
@@ -166,10 +166,10 @@ BEGIN
LeftNewMembers AS (
SELECT
cte.ParentId AS UserId,
COUNT(*) AS NewMembersCount
COUNT(DISTINCT cte.ChildId) AS NewMembersCount
FROM LeftLegCTE cte
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = cte.ChildId AND cm.IsActive = 1
WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate
INNER JOIN CMS.ClubMembershipCycles cc ON cc.UserId = cte.ChildId
WHERE cc.PackagePurchasedAt >= @StartDate AND cc.PackagePurchasedAt <= @EndDate
GROUP BY cte.ParentId
)
UPDATE b
@@ -203,10 +203,10 @@ BEGIN
RightNewMembers AS (
SELECT
cte.ParentId AS UserId,
COUNT(*) AS NewMembersCount
COUNT(DISTINCT cte.ChildId) AS NewMembersCount
FROM RightLegCTE cte
INNER JOIN CMS.ClubMemberships cm ON cm.UserId = cte.ChildId AND cm.IsActive = 1
WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate
INNER JOIN CMS.ClubMembershipCycles cc ON cc.UserId = cte.ChildId
WHERE cc.PackagePurchasedAt >= @StartDate AND cc.PackagePurchasedAt <= @EndDate
GROUP BY cte.ParentId
)
UPDATE b
@@ -3,10 +3,12 @@
-- Description: محاسبه استخر کمیسیون هفتگی و توزیع بین کاربران
-- Author: System
-- Created: 2025-12-19
-- Updated: Q29 — اضافه کردن @PackageId، fix فیلتر pool، مدیریت pool خالی
-- =============================================
CREATE OR ALTER PROCEDURE [CMS].[sp_CalculateWeeklyCommissionPool]
@WeekDefinitionId BIGINT,
@PackageId BIGINT, -- هر بار به ازای یک پکیج اجرا می‌شود
@ForceRecalculate BIT = 0,
@PoolId BIGINT OUTPUT
AS
@@ -15,7 +17,7 @@ BEGIN
SET XACT_ABORT ON;
DECLARE @ErrorMessage NVARCHAR(4000);
DECLARE @TotalPoolAmount BIGINT;
DECLARE @TotalPoolAmount BIGINT = 0;
DECLARE @TotalBalances INT;
DECLARE @ValuePerBalance BIGINT;
DECLARE @CalculatedAt DATETIME = GETDATE();
@@ -24,64 +26,73 @@ BEGIN
BEGIN TRANSACTION;
-- =============================================
-- 1. پیدا کردن Pool هفته
-- 1. پیدا کردن Pool هفته (فیلتر بر اساس PackageId)
-- =============================================
SELECT @PoolId = Id, @TotalPoolAmount = TotalPoolAmount
FROM CMS.WeeklyCommissionPools
WHERE WeekDefinitionId = @WeekDefinitionId;
WHERE WeekDefinitionId = @WeekDefinitionId AND PackageId = @PackageId;
-- اگر Pool وجود ندارد (هیچ فعال‌سازی در این هفته نبوده)، یک رکورد خالی ایجاد می‌کنیم
IF @PoolId IS NULL
BEGIN
SET @ErrorMessage = N'استخر کمیسیون برای هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' تعریف نشده است';
RAISERROR(@ErrorMessage, 16, 1);
RETURN;
INSERT INTO CMS.WeeklyCommissionPools (
WeekDefinitionId, PackageId, TotalPoolAmount, TotalBalances,
ValuePerBalance, IsCalculated, CalculatedAt,
IsDeleted, Created, CreatedBy, LastModified, LastModifiedBy
) VALUES (
@WeekDefinitionId, @PackageId, 0, 0, 0, 0, NULL,
0, @CalculatedAt, 'SP', @CalculatedAt, 'SP'
);
SET @PoolId = SCOPE_IDENTITY();
SET @TotalPoolAmount = 0;
END
-- =============================================
-- 2. بررسی محاسبات قبلی
-- 2. بررسی محاسبات قبلی (بر اساس همین Pool نه کل هفته)
-- =============================================
IF EXISTS (SELECT 1 FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId)
IF EXISTS (SELECT 1 FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId)
BEGIN
IF @ForceRecalculate = 0
BEGIN
SET @ErrorMessage = N'کمیسیون هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید';
SET @ErrorMessage = N'کمیسیون هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20))
+ N' (پکیج ' + CAST(@PackageId AS NVARCHAR(20)) + N') قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید';
RAISERROR(@ErrorMessage, 16, 1);
RETURN;
END
-- =============================================
-- 3. Revert کیف پول‌ها (در صورت ForceRecalculate)
-- 3. Revert کیف پول‌ها (در صورت ForceRecalculate، فقط پرداخت‌های این Pool)
-- =============================================
-- کم کردن مقادیر قبلی از NetworkBalance
UPDATE uw
SET uw.NetworkBalance = uw.NetworkBalance - ISNULL(
(SELECT SUM(wl.ChangeNerworkValue)
FROM CMS.UserWalletChangeLogs wl
WHERE wl.WalletId = uw.Id
AND wl.RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId)
(SELECT SUM(wl.ChangeNerworkValue)
FROM CMS.UserWalletChangeLogs wl
WHERE wl.WalletId = uw.Id
AND wl.RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId)
), 0)
FROM CMS.UserWallets uw
WHERE uw.Id IN (
SELECT DISTINCT wl.WalletId
FROM CMS.UserWalletChangeLogs wl
WHERE wl.RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId)
SELECT DISTINCT wl.WalletId
FROM CMS.UserWalletChangeLogs wl
WHERE wl.RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId)
);
-- حذف لاگ‌های تغییرات قبلی
DELETE FROM CMS.UserWalletChangeLogs
WHERE RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId);
DELETE FROM CMS.UserWalletChangeLogs
WHERE RefrenceId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId);
-- حذف تاریخچه پرداخت
DELETE FROM CMS.CommissionPayoutHistories
WHERE UserCommissionPayoutId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId);
DELETE FROM CMS.CommissionPayoutHistories
WHERE UserCommissionPayoutId IN (SELECT Id FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId);
-- حذف پرداخت‌های قبلی
DELETE FROM CMS.UserCommissionPayouts WHERE WeekDefinitionId = @WeekDefinitionId;
-- حذف پرداخت‌های قبلی این Pool
DELETE FROM CMS.UserCommissionPayouts WHERE WeeklyPoolId = @PoolId;
-- Reset وضعیت Pool
UPDATE CMS.WeeklyCommissionPools
SET IsCalculated = 0
UPDATE CMS.WeeklyCommissionPools
SET IsCalculated = 0
WHERE Id = @PoolId;
END
@@ -90,8 +101,12 @@ BEGIN
-- =============================================
IF NOT EXISTS (SELECT 1 FROM CMS.NetworkWeeklyBalances WHERE WeekDefinitionId = @WeekDefinitionId)
BEGIN
SET @ErrorMessage = N'ابتدا باید تعادل‌های هفتگی محاسبه شوند. sp_CalculateWeeklyBalances را اجرا کنید';
RAISERROR(@ErrorMessage, 16, 1);
-- بالانسی محاسبه نشده — pool را به عنوان "محاسبه شده بدون پرداخت" علامت می‌زنیم
UPDATE CMS.WeeklyCommissionPools
SET IsCalculated = 1, CalculatedAt = @CalculatedAt
WHERE Id = @PoolId;
COMMIT TRANSACTION;
RETURN;
END
@@ -104,11 +119,11 @@ BEGIN
IF @TotalBalances IS NULL OR @TotalBalances = 0
BEGIN
-- هیچ تعادلی برای توزیع وجود ندارد
UPDATE CMS.WeeklyCommissionPools
-- هیچ تعادلی برای توزیع وجود ندارد — pool محاسبه شده اما پرداختی ندارد
UPDATE CMS.WeeklyCommissionPools
SET IsCalculated = 1, CalculatedAt = @CalculatedAt
WHERE Id = @PoolId;
COMMIT TRANSACTION;
RETURN;
END
@@ -136,16 +151,16 @@ BEGIN
LastModified,
LastModifiedBy
)
SELECT
SELECT
nb.UserId,
@WeekDefinitionId,
@PoolId,
nb.TotalBalances,
@ValuePerBalance,
nb.TotalBalances * @ValuePerBalance,
0, -- Status: Pending
0, -- Status: Pending
NULL, -- PaidAt
0, -- IsDeleted
0, -- IsDeleted
@CalculatedAt,
'SP',
@CalculatedAt,
@@ -173,35 +188,35 @@ BEGIN
LastModified,
LastModifiedBy
)
SELECT
cp.Id, -- UserCommissionPayoutId
cp.UserId, -- UserId
SELECT
cp.Id,
cp.UserId,
@WeekDefinitionId,
0, -- AmountBefore
cp.TotalAmount, -- AmountAfter
0,
cp.TotalAmount,
0, -- OldStatus: Pending
0, -- NewStatus: Pending
0, -- Action: Created (assuming enum value)
'SP', -- PerformedBy
N'محاسبه اولیه توسط SP', -- Reason
0, -- IsDeleted
0, -- Action: Created
'SP',
N'محاسبه اولیه توسط SP',
0,
@CalculatedAt,
'SP',
@CalculatedAt,
'SP'
FROM CMS.UserCommissionPayouts cp
WHERE cp.WeekDefinitionId = @WeekDefinitionId;
WHERE cp.WeeklyPoolId = @PoolId;
-- =============================================
-- 9. شارژ کیف پول کاربران
-- =============================================
-- بروزرسانی NetworkBalance
UPDATE uw
SET uw.NetworkBalance = uw.NetworkBalance + cp.TotalAmount
FROM CMS.UserWallets uw
INNER JOIN CMS.UserCommissionPayouts cp ON cp.UserId = uw.UserId
WHERE cp.WeekDefinitionId = @WeekDefinitionId;
WHERE cp.WeeklyPoolId = @PoolId;
-- ثبت لاگ تغییرات کیف پول
INSERT INTO CMS.UserWalletChangeLogs (
@@ -220,24 +235,24 @@ BEGIN
LastModified,
LastModifiedBy
)
SELECT
SELECT
uw.Id,
uw.Balance, -- CurrentBalance
0, -- ChangeValue (no change to main balance)
uw.NetworkBalance, -- CurrentNetworkBalance (after update)
cp.TotalAmount, -- ChangeNerworkValue
uw.DiscountBalance, -- CurrentDiscountBalance
0, -- ChangeDiscountValue
1, -- IsIncrease: true
cp.Id, -- RefrenceId: PayoutId
0, -- IsDeleted
uw.Balance,
0, -- ChangeValue (no change to main balance)
uw.NetworkBalance, -- CurrentNetworkBalance (after update)
cp.TotalAmount, -- ChangeNerworkValue
uw.DiscountBalance,
0, -- ChangeDiscountValue
1, -- IsIncrease: true
cp.Id, -- RefrenceId: PayoutId
0,
@CalculatedAt,
'SP',
@CalculatedAt,
'SP'
FROM CMS.UserCommissionPayouts cp
INNER JOIN CMS.UserWallets uw ON uw.UserId = cp.UserId
WHERE cp.WeekDefinitionId = @WeekDefinitionId;
WHERE cp.WeeklyPoolId = @PoolId;
-- =============================================
-- 10. بروزرسانی Pool
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.193</Version>
<Version>0.0.195</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -212,6 +212,11 @@ message GetWeeklyCommissionPoolResponse
google.protobuf.Timestamp created = 9;
int64 package_id = 10;
string package_title = 11;
// Q29: اعتبارسنجی TotalPoolAmount
bool validation_is_valid = 12;
int64 validation_expected_amount = 13;
int64 validation_discrepancy = 14;
int32 validation_activation_count = 15;
}
// GetUserCommissionPayouts Query
@@ -205,6 +205,10 @@ message NetworkTreeNodeModel
string position = 16; // Root/Left/Right position indicator
NetworkTreeNodeModel left_child = 17; // Left child node
NetworkTreeNodeModel right_child = 18; // Right child node
// فیلدهای اطلاعات پکیج و نوع فعال‌سازی
google.protobuf.BoolValue is_new_activation = 22; // true=اولین فعال‌سازی، false=خرید مجدد، null=بدون Cycle
string package_name = 23; // نام پکیج موثر
google.protobuf.Int64Value package_id = 24; // شناسه پکیج
}
// GetHistory Query
@@ -51,7 +51,11 @@ public class CommissionProfile : IRegister
: null)
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created))
.Map(dest => dest.PackageId, src => src.PackageId)
.Map(dest => dest.PackageTitle, src => src.PackageTitle);
.Map(dest => dest.PackageTitle, src => src.PackageTitle)
.Map(dest => dest.ValidationIsValid, src => src.ValidationIsValid)
.Map(dest => dest.ValidationExpectedAmount, src => src.ValidationExpectedAmount)
.Map(dest => dest.ValidationDiscrepancy, src => src.ValidationDiscrepancy)
.Map(dest => dest.ValidationActivationCount, src => src.ValidationActivationCount);
// GetAvailableWeeks Response Mapping
config.NewConfig<GetAvailableWeeksResponseDto, GetAvailableWeeksResponse>()
@@ -0,0 +1,75 @@
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProducts;
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using Google.Protobuf.WellKnownTypes;
using Mapster;
using ProtoDiscountProductDto = CMSMicroservice.Protobuf.Protos.DiscountProduct.DiscountProductDto;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class DiscountProductProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// GetDiscountProductsRequest (proto) → GetDiscountProductsQuery (CQRS)
config.NewConfig<GetDiscountProductsRequest, GetDiscountProductsQuery>()
.Map(dest => dest.SearchTerm,
src => string.IsNullOrEmpty(src.SearchQuery) ? null : src.SearchQuery)
.Map(dest => dest.CategoryId,
src => src.CategoryId != null ? src.CategoryId.Value : (long?)null)
.Map(dest => dest.IsActive,
src => src.IsActive != null ? src.IsActive.Value : (bool?)null)
.Map(dest => dest.MinPrice,
src => src.MinPrice != null ? (int?)src.MinPrice.Value : null)
.Map(dest => dest.MaxPrice,
src => src.MaxPrice != null ? (int?)src.MaxPrice.Value : null)
.Map(dest => dest.PaginationQuery, src => new PaginationState
{
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
PageSize = src.PageSize > 0 ? src.PageSize : 12
});
// GetDiscountProductsResponseDto (CQRS) → GetDiscountProductsResponse (proto)
config.NewConfig<GetDiscountProductsResponseDto, GetDiscountProductsResponse>()
.MapWith(src => MapToResponse(src));
}
private static GetDiscountProductsResponse MapToResponse(GetDiscountProductsResponseDto src)
{
var response = new GetDiscountProductsResponse();
if (src.MetaData != null)
{
response.MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
TotalCount = src.MetaData.TotalCount,
PageSize = src.MetaData.PageSize,
CurrentPage = src.MetaData.CurrentPage,
TotalPage = src.MetaData.TotalPage
};
}
if (src.Models != null)
{
foreach (var p in src.Models)
{
response.Models.Add(new ProtoDiscountProductDto
{
Id = p.Id,
Title = p.Title ?? string.Empty,
ShortInfomation = p.ShortInfomation ?? string.Empty,
Price = p.Price,
MaxDiscountPercent = p.MaxDiscountPercent,
ImagePath = p.ImagePath ?? string.Empty,
ThumbnailPath = p.ThumbnailPath ?? string.Empty,
RemainingCount = p.RemainingCount,
ViewCount = p.ViewCount,
IsActive = p.IsActive,
Created = Timestamp.FromDateTime(DateTime.UtcNow)
});
}
}
return response;
}
}
@@ -92,7 +92,10 @@ public class NetworkMembershipProfile : IRegister
IsActive = true,
IsClubActive = node.IsClubActive,
ActivationWeekDefinitionId = node.ActivationWeekDefinitionId ?? 0,
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek,
PackageName = node.PackageName ?? string.Empty,
PackageId = node.PackageId.HasValue ? node.PackageId.Value : null,
IsNewActivation = node.IsNewActivation.HasValue ? node.IsNewActivation.Value : null
};
if (parentId.HasValue)