fix: package-based compliance audit — feature DIFF, remove hardcoded fallbacks
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m12s
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m12s
- 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)
This commit is contained in:
+57
-30
@@ -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,
|
||||
|
||||
+80
-40
@@ -221,16 +221,20 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existingMembership.IsActive)
|
||||
// بررسی آیا چرخه فعال دارد — اگر بله، واقعاً فعال است و نیازی به کار نیست
|
||||
var hasCurrentCycle = await _context.ClubMembershipCycles
|
||||
.AnyAsync(c => 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<ActivateClub
|
||||
_context.ClubMemberships.Update(entity);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Reactivated club membership for UserId {UserId}",
|
||||
user.Id
|
||||
"Re-activating club membership for UserId {UserId} (re-purchase scenario, IsActive was {WasActive})",
|
||||
user.Id,
|
||||
existingMembership.IsActive
|
||||
);
|
||||
}
|
||||
|
||||
@@ -343,41 +348,8 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 9. اضافه کردن ویژگیهای باشگاه برای کاربر (فقط برای عضویت جدید)
|
||||
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 = 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<ActivateClub
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// اعمال DIFF فیچرها بر اساس پکیج (Q20)
|
||||
/// عضویت جدید: همه فیچرهای پکیج اضافه میشه
|
||||
/// خرید مجدد: تفاضل فیچرهای فعلی و پکیج جدید
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت شناسه تعریف هفته جاری
|
||||
/// </summary>
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
|
||||
@@ -20,42 +20,6 @@ public static class SystemConstants
|
||||
|
||||
#endregion
|
||||
|
||||
#region Club Settings
|
||||
|
||||
/// <summary>
|
||||
/// مبلغ هدیه حق عضویت باشگاه (ریال) - این مبلغ از کیف پول کم نمیشود
|
||||
/// [DEPRECATED] از Package.Price استفاده کنید
|
||||
/// </summary>
|
||||
[Obsolete("Moved to Package entity. Use package.Price or package.ActivationFee instead.")]
|
||||
public const long ClubMembershipGiftValue = 25_200_000;
|
||||
|
||||
/// <summary>
|
||||
/// هزینه فعالسازی عضویت باشگاه (ریال)
|
||||
/// [DEPRECATED] از Package.ActivationFee استفاده کنید
|
||||
/// </summary>
|
||||
[Obsolete("Moved to Package.ActivationFee. Read from Package entity instead.")]
|
||||
public const long ClubActivationFee = 25_200_000;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Package Settings
|
||||
|
||||
/// <summary>
|
||||
/// مبلغ پکیج پایه (ریال)
|
||||
/// [DEPRECATED] از Package.Price استفاده کنید
|
||||
/// </summary>
|
||||
[Obsolete("Moved to Package.Price. Read from Package entity instead.")]
|
||||
public const long BasePackageAmount = 56_000_000;
|
||||
|
||||
/// <summary>
|
||||
/// مبلغ وام دایا (ریال)
|
||||
/// [DEPRECATED] از Package.Price استفاده کنید
|
||||
/// </summary>
|
||||
[Obsolete("Moved to Package.Price. Read from Package entity with SupportsDayaPurchase=true.")]
|
||||
public const long DayaLoanAmount = 56_000_000;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Commission Settings
|
||||
|
||||
/// <summary>
|
||||
@@ -68,20 +32,6 @@ public static class SystemConstants
|
||||
/// </summary>
|
||||
public const long CommissionMinWithdrawalAmount = 1_000_000;
|
||||
|
||||
/// <summary>
|
||||
/// سقف تعادل هفتگی برای هر دست (چپ یا راست) - حداکثر کل = 600
|
||||
/// [DEPRECATED] از Package.MaxBalancesPerLeg استفاده کنید
|
||||
/// </summary>
|
||||
[Obsolete("Moved to Package.MaxBalancesPerLeg. Read from Package entity instead.")]
|
||||
public const int CommissionMaxWeeklyBalancesPerLeg = 300;
|
||||
|
||||
/// <summary>
|
||||
/// حداکثر عمق شبکه برای محاسبه کمیسیون (تعداد لول زیرمجموعه)
|
||||
/// [DEPRECATED] از Package.MaxNetworkLevel استفاده کنید
|
||||
/// </summary>
|
||||
[Obsolete("Moved to Package.MaxNetworkLevel. Read from Package entity instead.")]
|
||||
public const int CommissionMaxNetworkLevel = 15;
|
||||
|
||||
/// <summary>
|
||||
/// روش محاسبه (ORM یا SP)
|
||||
/// </summary>
|
||||
@@ -105,26 +55,8 @@ public static class SystemConstants
|
||||
|
||||
#region Magic Wallet Settings
|
||||
|
||||
/// <summary>
|
||||
/// ضریب شارژ کیفپول جادویی — واریز × 2.5 = اعتبار
|
||||
/// [DEPRECATED] از Package.MagicWalletMultiplier استفاده کنید
|
||||
/// </summary>
|
||||
[Obsolete("Moved to Package.MagicWalletMultiplier. Read from Package entity instead.")]
|
||||
public const decimal MagicWalletMultiplier = 2.5m;
|
||||
|
||||
/// <summary>
|
||||
/// سقف واریز در هر دور جادویی (ریال) — 100M تومان
|
||||
/// [DEPRECATED] از Package.MagicWalletMaxDeposit استفاده کنید
|
||||
/// </summary>
|
||||
[Obsolete("Moved to Package.MagicWalletMaxDeposit. Read from Package entity instead.")]
|
||||
public const long MagicWalletMaxDeposit = 1_000_000_000;
|
||||
|
||||
/// <summary>
|
||||
/// سقف اعتبار در هر دور جادویی (ریال) — 250M تومان
|
||||
/// [DEPRECATED] از Package.MagicWalletMaxCredit استفاده کنید
|
||||
/// </summary>
|
||||
[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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user