From 1ac23667e2e69a9992dd0ddb0013e03573198132 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 27 Feb 2026 02:35:41 +0330 Subject: [PATCH] =?UTF-8?q?fix:=20package-based=20compliance=20audit=20?= =?UTF-8?q?=E2=80=94=20feature=20DIFF,=20remove=20hardcoded=20fallbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ActivateClubMembership: replace GetAllFeatureIds with PackageFeature DIFF (Q20) - ActivateClubMembership: fix re-activation early return — check current cycle - AcceptClubMembershipContract: same feature DIFF logic - VerifyPackagePurchase: remove ?? 2.0m fallback, throw if package null - PackageService: remove ?? 2.0m fallback for DiscountMultiplier - UserOrderService: remove ?? 1B fallback for MagicWalletMaxDeposit - SystemConstants: remove 9 dead [Obsolete] constants (per-package values) --- ...eptClubMembershipContractCommandHandler.cs | 87 ++++++++----- .../ActivateClubMembershipCommandHandler.cs | 120 ++++++++++++------ .../VerifyPackagePurchaseCommandHandler.cs | 4 +- .../Common/SystemConstants.cs | 72 +---------- .../Services/PackageService.cs | 4 +- .../Services/UserOrderService.cs | 3 +- 6 files changed, 147 insertions(+), 143 deletions(-) diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs index 5e403c8..a899d9a 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/AcceptClubMembershipContract/AcceptClubMembershipContractCommandHandler.cs @@ -262,36 +262,8 @@ public class AcceptClubMembershipContractCommandHandler await _context.SaveChangesAsync(cancellationToken); - // 10. اعطای ویژگی‌های باشگاه برای کاربر (فقط برای عضویت جدید) - if (isNewMembership) - { - var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds(); - var clubFeatures = await _context.ClubFeatures - .Where(f => !f.IsDeleted && featureIds.Contains(f.Id)) - .ToListAsync(cancellationToken); - - if (clubFeatures.Any()) - { - var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature - { - UserId = user.Id, - ClubMembershipId = clubMembership.Id, - ClubFeatureId = feature.Id, - GrantedAt = activationDate, - IsActive = true, - Notes = "اعطا شده هنگام امضای قرارداد" - }).ToList(); - - _context.UserClubFeatures.AddRange(userClubFeatures); - await _context.SaveChangesAsync(cancellationToken); - - _logger.LogInformation( - "Granted {Count} club features to UserId {UserId}", - clubFeatures.Count, - user.Id - ); - } - } + // 10. اعمال ویژگی‌های پکیج — DIFF/تفاضل (Q20) + await ApplyFeatureDiffAsync(user.Id, clubMembership.Id, package.Id, isNewMembership, activationDate, cancellationToken); _logger.LogInformation( "Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}, MembershipId: {MembershipId}", @@ -321,6 +293,61 @@ public class AcceptClubMembershipContractCommandHandler return week.Id; } + private async Task ApplyFeatureDiffAsync( + long userId, long membershipId, long packageId, + bool isNewMembership, DateTime activationDate, + CancellationToken cancellationToken) + { + var newFeatureIds = await _context.PackageFeatures + .Where(pf => pf.PackageId == packageId && pf.IsIncluded && !pf.IsDeleted) + .Select(pf => pf.ClubFeatureId) + .ToListAsync(cancellationToken); + + if (!newFeatureIds.Any()) + { + _logger.LogWarning("No PackageFeatures for PackageId {PackageId}", packageId); + return; + } + + var currentFeatures = await _context.UserClubFeatures + .Where(ucf => ucf.UserId == userId && ucf.IsActive) + .ToListAsync(cancellationToken); + + var currentFeatureIds = currentFeatures.Select(f => f.ClubFeatureId).ToHashSet(); + var toAdd = newFeatureIds.Where(id => !currentFeatureIds.Contains(id)).ToList(); + var toRemove = currentFeatures.Where(f => !newFeatureIds.Contains(f.ClubFeatureId)).ToList(); + + foreach (var feature in toRemove) + { + feature.IsActive = false; + feature.Notes = $"حذف شده بابت تغییر پکیج (PackageId: {packageId})"; + } + + if (toAdd.Any()) + { + var newUserFeatures = toAdd.Select(featureId => new UserClubFeature + { + UserId = userId, + ClubMembershipId = membershipId, + ClubFeatureId = featureId, + GrantedAt = activationDate, + IsActive = true, + Notes = isNewMembership + ? "اعطا شده هنگام امضای قرارداد" + : $"اضافه شده بابت خرید مجدد پکیج (PackageId: {packageId})" + }).ToList(); + + _context.UserClubFeatures.AddRange(newUserFeatures); + } + + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Feature DIFF applied for UserId={UserId}: Added={Added}, Removed={Removed}, Kept={Kept}", + userId, toAdd.Count, toRemove.Count, + currentFeatureIds.Count - toRemove.Count); + } + private async Task<(bool Success, string Message)> VerifyOtpAsync( string mobile, string code, diff --git a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs index 91a6e43..b7842e1 100644 --- a/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs +++ b/src/CMSMicroservice.Application/ClubMembershipCQ/Commands/ActivateClubMembership/ActivateClubMembershipCommandHandler.cs @@ -221,16 +221,20 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler c.UserId == user.Id && c.IsCurrentCycle, cancellationToken); + + if (existingMembership.IsActive && hasCurrentCycle) { _logger.LogInformation( - "User {UserId} is already an active club member", + "User {UserId} is already an active club member with a running cycle", user.Id ); return true; } - // فعال‌سازی مجدد — FirstActivation حفظ میشه (Q21), Last بروزرسانی میشه + // فعال‌سازی مجدد یا خرید مجدد (Q19/Q20) — FirstActivation حفظ میشه (Q21), Last بروزرسانی میشه entity = existingMembership; entity.IsActive = true; entity.LastActivationDate = activationDate; @@ -240,8 +244,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler !f.IsDeleted && featureIds.Contains(f.Id)) - .ToListAsync(cancellationToken); - - if (clubFeatures.Any()) - { - var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature - { - UserId = user.Id, - ClubMembershipId = entity.Id, - ClubFeatureId = feature.Id, - GrantedAt = activationDate, - IsActive = true, - Notes = "اعطا شده به‌طور خودکار هنگام فعالسازی" - }).ToList(); _context.UserClubFeatures.AddRange(userClubFeatures); - await _context.SaveChangesAsync(cancellationToken); - - _logger.LogInformation( - "Granted {Count} club features to UserId {UserId}", - clubFeatures.Count, - user.Id - ); - } - else - { - _logger.LogWarning( - "No club features found to grant to UserId {UserId}", - user.Id - ); - } - } + // 9. اعمال ویژگی‌های پکیج — DIFF/تفاضل (Q20) + await ApplyFeatureDiffAsync(user.Id, entity.Id, package.Id, isNewMembership, activationDate, cancellationToken); _logger.LogInformation( "Club membership activated successfully. UserId: {UserId}, MembershipId: {MembershipId}", @@ -398,6 +370,74 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler + /// اعمال DIFF فیچرها بر اساس پکیج (Q20) + /// عضویت جدید: همه فیچرهای پکیج اضافه میشه + /// خرید مجدد: تفاضل فیچرهای فعلی و پکیج جدید + /// + private async Task ApplyFeatureDiffAsync( + long userId, long membershipId, long packageId, + bool isNewMembership, DateTime activationDate, + CancellationToken cancellationToken) + { + // فیچرهای پکیج جدید + var newFeatureIds = await _context.PackageFeatures + .Where(pf => pf.PackageId == packageId && pf.IsIncluded && !pf.IsDeleted) + .Select(pf => pf.ClubFeatureId) + .ToListAsync(cancellationToken); + + if (!newFeatureIds.Any()) + { + _logger.LogWarning( + "No PackageFeatures found for PackageId {PackageId} — skipping feature assignment for UserId {UserId}", + packageId, userId); + return; + } + + // فیچرهای فعال فعلی کاربر + var currentFeatures = await _context.UserClubFeatures + .Where(ucf => ucf.UserId == userId && ucf.IsActive) + .ToListAsync(cancellationToken); + + var currentFeatureIds = currentFeatures.Select(f => f.ClubFeatureId).ToHashSet(); + + // DIFF + var toAdd = newFeatureIds.Where(id => !currentFeatureIds.Contains(id)).ToList(); + var toRemove = currentFeatures.Where(f => !newFeatureIds.Contains(f.ClubFeatureId)).ToList(); + + // حذف فیچرهای قدیمی که پکیج جدید ندارد + foreach (var feature in toRemove) + { + feature.IsActive = false; + feature.Notes = $"حذف شده بابت تغییر پکیج (PackageId: {packageId})"; + } + + // اضافه فیچرهای جدید + if (toAdd.Any()) + { + var newUserFeatures = toAdd.Select(featureId => new UserClubFeature + { + UserId = userId, + ClubMembershipId = membershipId, + ClubFeatureId = featureId, + GrantedAt = activationDate, + IsActive = true, + Notes = isNewMembership + ? "اعطا شده هنگام فعالسازی اولیه" + : $"اضافه شده بابت خرید مجدد پکیج (PackageId: {packageId})" + }).ToList(); + + _context.UserClubFeatures.AddRange(newUserFeatures); + } + + await _context.SaveChangesAsync(cancellationToken); + + _logger.LogInformation( + "Feature DIFF applied for UserId={UserId}: Added={Added}, Removed={Removed}, Kept={Kept}", + userId, toAdd.Count, toRemove.Count, + currentFeatureIds.Count - toRemove.Count); + } + /// /// دریافت شناسه تعریف هفته جاری /// diff --git a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyPackagePurchase/VerifyPackagePurchaseCommandHandler.cs b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyPackagePurchase/VerifyPackagePurchaseCommandHandler.cs index 8ca5177..419eb03 100644 --- a/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyPackagePurchase/VerifyPackagePurchaseCommandHandler.cs +++ b/src/CMSMicroservice.Application/PackageCQ/Commands/VerifyPackagePurchase/VerifyPackagePurchaseCommandHandler.cs @@ -101,7 +101,9 @@ public class VerifyPackagePurchaseCommandHandler // شارژ DiscountBalance (موجودی تخفیف) — ضریب تخفیف از پکیج var oldDiscountBalance = wallet.DiscountBalance; - var discountMultiplier = order.Package?.DiscountMultiplier ?? 2.0m; + if (order.Package == null) + throw new NotFoundException("پکیج مرتبط با سفارش یافت نشد"); + var discountMultiplier = order.Package.DiscountMultiplier; var discountAmount = (long)(order.Amount * discountMultiplier); wallet.DiscountBalance += discountAmount; diff --git a/src/CMSMicroservice.Domain/Common/SystemConstants.cs b/src/CMSMicroservice.Domain/Common/SystemConstants.cs index e424383..662d79b 100644 --- a/src/CMSMicroservice.Domain/Common/SystemConstants.cs +++ b/src/CMSMicroservice.Domain/Common/SystemConstants.cs @@ -20,42 +20,6 @@ public static class SystemConstants #endregion - #region Club Settings - - /// - /// مبلغ هدیه حق عضویت باشگاه (ریال) - این مبلغ از کیف پول کم نمی‌شود - /// [DEPRECATED] از Package.Price استفاده کنید - /// - [Obsolete("Moved to Package entity. Use package.Price or package.ActivationFee instead.")] - public const long ClubMembershipGiftValue = 25_200_000; - - /// - /// هزینه فعال‌سازی عضویت باشگاه (ریال) - /// [DEPRECATED] از Package.ActivationFee استفاده کنید - /// - [Obsolete("Moved to Package.ActivationFee. Read from Package entity instead.")] - public const long ClubActivationFee = 25_200_000; - - #endregion - - #region Package Settings - - /// - /// مبلغ پکیج پایه (ریال) - /// [DEPRECATED] از Package.Price استفاده کنید - /// - [Obsolete("Moved to Package.Price. Read from Package entity instead.")] - public const long BasePackageAmount = 56_000_000; - - /// - /// مبلغ وام دایا (ریال) - /// [DEPRECATED] از Package.Price استفاده کنید - /// - [Obsolete("Moved to Package.Price. Read from Package entity with SupportsDayaPurchase=true.")] - public const long DayaLoanAmount = 56_000_000; - - #endregion - #region Commission Settings /// @@ -68,20 +32,6 @@ public static class SystemConstants /// public const long CommissionMinWithdrawalAmount = 1_000_000; - /// - /// سقف تعادل هفتگی برای هر دست (چپ یا راست) - حداکثر کل = 600 - /// [DEPRECATED] از Package.MaxBalancesPerLeg استفاده کنید - /// - [Obsolete("Moved to Package.MaxBalancesPerLeg. Read from Package entity instead.")] - public const int CommissionMaxWeeklyBalancesPerLeg = 300; - - /// - /// حداکثر عمق شبکه برای محاسبه کمیسیون (تعداد لول زیرمجموعه) - /// [DEPRECATED] از Package.MaxNetworkLevel استفاده کنید - /// - [Obsolete("Moved to Package.MaxNetworkLevel. Read from Package entity instead.")] - public const int CommissionMaxNetworkLevel = 15; - /// /// روش محاسبه (ORM یا SP) /// @@ -105,26 +55,8 @@ public static class SystemConstants #region Magic Wallet Settings - /// - /// ضریب شارژ کیف‌پول جادویی — واریز × 2.5 = اعتبار - /// [DEPRECATED] از Package.MagicWalletMultiplier استفاده کنید - /// - [Obsolete("Moved to Package.MagicWalletMultiplier. Read from Package entity instead.")] - public const decimal MagicWalletMultiplier = 2.5m; - - /// - /// سقف واریز در هر دور جادویی (ریال) — 100M تومان - /// [DEPRECATED] از Package.MagicWalletMaxDeposit استفاده کنید - /// - [Obsolete("Moved to Package.MagicWalletMaxDeposit. Read from Package entity instead.")] - public const long MagicWalletMaxDeposit = 1_000_000_000; - - /// - /// سقف اعتبار در هر دور جادویی (ریال) — 250M تومان - /// [DEPRECATED] از Package.MagicWalletMaxCredit استفاده کنید - /// - [Obsolete("Moved to Package.MagicWalletMaxCredit. Read from Package entity instead.")] - public const long MagicWalletMaxCredit = 2_500_000_000; + // Per-package magic wallet settings are now in Package entity: + // Package.MagicWalletMultiplier, Package.MagicWalletMaxDeposit, Package.MagicWalletMaxCredit #endregion diff --git a/src/CMSMicroservice.WebApi/Services/PackageService.cs b/src/CMSMicroservice.WebApi/Services/PackageService.cs index b8dcf21..2effbe3 100644 --- a/src/CMSMicroservice.WebApi/Services/PackageService.cs +++ b/src/CMSMicroservice.WebApi/Services/PackageService.cs @@ -314,7 +314,9 @@ public class PackageService : PackageContract.PackageContractBase await _context.SaveChangesAsync(context.CancellationToken); } - var discountAmount = (long)(purchase.Amount * (double)(purchase.Package?.DiscountMultiplier ?? 2.0m)); + var discountMultiplier = purchase.Package?.DiscountMultiplier + ?? throw new RpcException(new Status(StatusCode.Internal, "پکیج سفارش یافت نشد")); + var discountAmount = (long)(purchase.Amount * (double)discountMultiplier); wallet.Balance += purchase.Amount; wallet.DiscountBalance += discountAmount; diff --git a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs index 81eca8d..113e5b6 100644 --- a/src/CMSMicroservice.WebApi/Services/UserOrderService.cs +++ b/src/CMSMicroservice.WebApi/Services/UserOrderService.cs @@ -352,7 +352,8 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase var userPackage = userCycle != null ? await _context.Packages.FirstOrDefaultAsync(p => p.Id == userCycle.PackageId, context.CancellationToken) : await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, context.CancellationToken); - var magicMaxDeposit = userPackage?.MagicWalletMaxDeposit ?? 1_000_000_000; + var magicMaxDeposit = userPackage?.MagicWalletMaxDeposit + ?? throw new InvalidOperationException($"Package not found for magic wallet exit check, UserId={userId}"); if (wallet.MagicTotalDeposited >= magicMaxDeposit) {