diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs index a899d9a..a73731f 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs @@ -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); diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommand.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommand.cs index 2706c91..63e5be8 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommand.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommand.cs @@ -17,4 +17,10 @@ public record ActivateClubMembershipCommand : IRequest /// فعال‌سازی اجباری توسط ادمین — بدون بررسی سفارش و کیف‌پول /// public bool ForceActivation { get; init; } + + /// + /// شناسه پکیج — فقط در ForceActivation استفاده می‌شود. + /// اگر 0 باشد، پکیج پایه (IsBasePackage) استفاده می‌شود. + /// + public long PackageId { get; init; } } diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs index b960c41..d3c1c14 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs @@ -170,10 +170,19 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler 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 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 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); diff --git a/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs b/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs index eeae512..32f85a7 100644 --- a/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs +++ b/src/CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/CalculateWeeklyBalancesCommandHandler.cs @@ -266,12 +266,16 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler 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; } diff --git a/src/CMSMicroservice.Application/CommissionCQ/Queries/GetWeeklyCommissionPool/GetWeeklyCommissionPoolQueryHandler.cs b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetWeeklyCommissionPool/GetWeeklyCommissionPoolQueryHandler.cs index ebd6d85..957bc16 100644 --- a/src/CMSMicroservice.Application/CommissionCQ/Queries/GetWeeklyCommissionPool/GetWeeklyCommissionPoolQueryHandler.cs +++ b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetWeeklyCommissionPool/GetWeeklyCommissionPoolQueryHandler.cs @@ -41,6 +41,61 @@ public class GetWeeklyCommissionPoolQueryHandler : IRequestHandler + /// TotalPoolAmount استخر را با تعداد فعال‌سازی‌های ثبت‌شده (AcceptContract + ActivateClubMembership) مقایسه می‌کند. + /// نتیجه در فیلدهای Validation... ذخیره می‌شود. + /// + 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; } } diff --git a/src/CMSMicroservice.Application/CommissionCQ/Queries/GetWeeklyCommissionPool/WeeklyCommissionPoolDto.cs b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetWeeklyCommissionPool/WeeklyCommissionPoolDto.cs index f21d481..32e4b88 100644 --- a/src/CMSMicroservice.Application/CommissionCQ/Queries/GetWeeklyCommissionPool/WeeklyCommissionPoolDto.cs +++ b/src/CMSMicroservice.Application/CommissionCQ/Queries/GetWeeklyCommissionPool/WeeklyCommissionPoolDto.cs @@ -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) ── + /// آیا TotalPoolAmount با تعداد فعال‌سازی‌های ثبت‌شده در دیتابیس مطابقت دارد؟ + public bool ValidationIsValid { get; set; } = true; + /// مبلغ مورد انتظار بر اساس شمارش فعال‌سازی‌ها × ActivationFee + public long ValidationExpectedAmount { get; set; } + /// اختلاف: TotalPoolAmount - ValidationExpectedAmount + public long ValidationDiscrepancy { get; set; } + /// تعداد فعال‌سازی‌های شناسایی‌شده برای این هفته و پکیج + public int ValidationActivationCount { get; set; } } diff --git a/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs b/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs index fa7fb68..2576efd 100644 --- a/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs +++ b/src/CMSMicroservice.Application/ManualPaymentCQ/Commands/CreateManualPayment/CreateManualPaymentCommandHandler.cs @@ -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 _logger; public CreateManualPaymentCommandHandler( IApplicationDbContext context, ICurrentUserService currentUser, + ISender sender, ILogger logger) { _context = context; _currentUser = currentUser; + _sender = sender; _logger = logger; } @@ -152,6 +156,29 @@ public class CreateManualPaymentCommandHandler : IRequestHandler public DateTimeOffset UserCreated { get; set; } - + + /// + /// آیا فعال‌سازی در هفته هدف اولین بار بوده؟ null = بدون Cycle + /// + public bool? IsNewActivation { get; set; } + + /// + /// نام پکیج موثر (هفته هدف یا پکیج فعلی) + /// + public string? PackageName { get; set; } + + public long? PackageId { get; set; } + public NetworkTreeDto? LeftChild { get; set; } public NetworkTreeDto? RightChild { get; set; } } diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeNodeDto.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeNodeDto.cs index 53ab37f..1b6c33e 100644 --- a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeNodeDto.cs +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeNodeDto.cs @@ -20,6 +20,19 @@ public class NetworkTreeNodeDto public string? ActivationWeekDisplayName { get; set; } public bool IsActivatedInTargetWeek { get; set; } public DateTimeOffset UserCreated { get; set; } + + /// + /// آیا فعال‌سازی در هفته هدف اولین بار بوده (true) یا خرید مجدد (false)؟ + /// null یعنی Cycle‌ای ندارد + /// + public bool? IsNewActivation { get; set; } + + /// + /// نام پکیج موثر (هفته هدف یا پکیج فعلی) + /// + public string? PackageName { get; set; } + + public long? PackageId { get; set; } /// /// نام کامل کاربر diff --git a/src/CMSMicroservice.Infrastructure/Persistence/StoredProcedures/sp_CalculateWeeklyBalances.sql b/src/CMSMicroservice.Infrastructure/Persistence/StoredProcedures/sp_CalculateWeeklyBalances.sql index 8a8ae02..cc1428f 100644 --- a/src/CMSMicroservice.Infrastructure/Persistence/StoredProcedures/sp_CalculateWeeklyBalances.sql +++ b/src/CMSMicroservice.Infrastructure/Persistence/StoredProcedures/sp_CalculateWeeklyBalances.sql @@ -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 diff --git a/src/CMSMicroservice.Infrastructure/Persistence/StoredProcedures/sp_CalculateWeeklyCommissionPool.sql b/src/CMSMicroservice.Infrastructure/Persistence/StoredProcedures/sp_CalculateWeeklyCommissionPool.sql index 458f6a8..c54a3bf 100644 --- a/src/CMSMicroservice.Infrastructure/Persistence/StoredProcedures/sp_CalculateWeeklyCommissionPool.sql +++ b/src/CMSMicroservice.Infrastructure/Persistence/StoredProcedures/sp_CalculateWeeklyCommissionPool.sql @@ -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 diff --git a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj index c0134b2..f857e88 100644 --- a/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj +++ b/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj @@ -3,7 +3,7 @@ net9.0 enable enable - 0.0.193 + 0.0.195 None False False diff --git a/src/CMSMicroservice.Protobuf/Protos/commission.proto b/src/CMSMicroservice.Protobuf/Protos/commission.proto index 51c37bc..87d96cd 100644 --- a/src/CMSMicroservice.Protobuf/Protos/commission.proto +++ b/src/CMSMicroservice.Protobuf/Protos/commission.proto @@ -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 diff --git a/src/CMSMicroservice.Protobuf/Protos/networkmembership.proto b/src/CMSMicroservice.Protobuf/Protos/networkmembership.proto index 45ca30b..6f7ec5d 100644 --- a/src/CMSMicroservice.Protobuf/Protos/networkmembership.proto +++ b/src/CMSMicroservice.Protobuf/Protos/networkmembership.proto @@ -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 diff --git a/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs b/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs index 79b9144..ec6daf5 100644 --- a/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs +++ b/src/CMSMicroservice.WebApi/Common/Mappings/CommissionProfile.cs @@ -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() diff --git a/src/CMSMicroservice.WebApi/Common/Mappings/DiscountProductProfile.cs b/src/CMSMicroservice.WebApi/Common/Mappings/DiscountProductProfile.cs new file mode 100644 index 0000000..d80be07 --- /dev/null +++ b/src/CMSMicroservice.WebApi/Common/Mappings/DiscountProductProfile.cs @@ -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() + .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() + .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; + } +} diff --git a/src/CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs b/src/CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs index e50fe94..1c3913e 100644 --- a/src/CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs +++ b/src/CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs @@ -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)