using CMSMicroservice.Application.Common.Exceptions; using CMSMicroservice.Application.Common.Helpers; using CMSMicroservice.Application.Common.Interfaces; using CMSMicroservice.Application.Common.Models; using CMSMicroservice.Domain.Common; using CMSMicroservice.Domain.Entities; using CMSMicroservice.Domain.Entities.Club; using CMSMicroservice.Domain.Entities.Commission; using CMSMicroservice.Domain.Entities.History; using CMSMicroservice.Domain.Enums; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System.Globalization; namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership; public class ActivateClubMembershipCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly ICurrentUserService _currentUser; private readonly IWeekDefinitionRepository _weekRepository; private readonly ILogger _logger; public ActivateClubMembershipCommandHandler( IApplicationDbContext context, ICurrentUserService currentUser, IWeekDefinitionRepository weekRepository, ILogger logger) { _context = context; _currentUser = currentUser; _weekRepository = weekRepository; _logger = logger; } public async Task Handle( ActivateClubMembershipCommand request, CancellationToken cancellationToken) { try { _logger.LogInformation( "Activating club membership for UserId: {UserId}", request.UserId ); // 1. بررسی کاربر var user = await _context.Users .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken); if (user == null) { _logger.LogWarning("User not found: {UserId}", request.UserId); throw new NotFoundException(nameof(User), request.UserId); } // متغیر پکیج — در هر دو مسیر (عادی و Force) مقداردهی می‌شود Package package; // 2-5: بررسی‌های مالی — در حالت ForceActivation (ادمین) رد می‌شود if (!request.ForceActivation) { // 2. بررسی اینکه پکیج خریده باشد if (user.PackagePurchaseMethod == PackagePurchaseMethod.None) { _logger.LogWarning( "User {UserId} has not purchased any package yet", request.UserId ); throw new BadRequestException( "برای فعالسازی باشگاه مشتریان ابتدا باید یک پکیج خریداری کنید" ); } // 3. بررسی موجودی کیف پول var wallet = await _context.UserWallets .FirstOrDefaultAsync(w => w.UserId == user.Id, cancellationToken); if (wallet == null) { _logger.LogError("Wallet not found for UserId: {UserId}", request.UserId); throw new NotFoundException("کیف پول کاربر یافت نشد"); } // NOTE: balance check uses package.Price — loaded after finding order (step 4) // Moved to after package loading // 3.5. بررسی وضعیت کیف‌پول جادویی — اگر در حالت Magic است، فعالسازی مجاز نیست if (wallet.WalletMode == WalletMode.Magic) { _logger.LogWarning( "User {UserId} cannot activate club while in Magic wallet mode", request.UserId ); throw new BadRequestException( "ابتدا باید چرخه کیف‌پول جادویی تکمیل شود" ); } // 4. پیدا کردن آخرین خرید موفق پکیج var packagePurchase = await _context.UserPackagePurchases .Include(p => p.Transaction) .Where(p => p.UserId == user.Id && p.Transaction != null && p.Transaction.PaymentStatus == PaymentStatus.Success) .OrderByDescending(p => p.Created) .FirstOrDefaultAsync(cancellationToken); if (packagePurchase == null) { _logger.LogWarning( "No successful package purchase found for UserId: {UserId}", request.UserId ); throw new NotFoundException("سفارش پکیج یافت نشد"); } // 5. بررسی Transaction if (packagePurchase.Transaction == null) { _logger.LogError( "Transaction not found for PurchaseId: {PurchaseId}", packagePurchase.Id ); throw new NotFoundException("تراکنش مربوط به سفارش یافت نشد"); } var transaction = packagePurchase.Transaction; if (transaction.Type != TransactionType.Buy && transaction.Type != TransactionType.DepositIpg && transaction.Type != TransactionType.DepositExternal1) { _logger.LogWarning( "Invalid transaction type for PurchaseId {PurchaseId}: {Type}", packagePurchase.Id, transaction.Type ); throw new BadRequestException( "تراکنش معتبر برای فعالسازی باشگاه یافت نشد" ); } // 5.5. بارگذاری پکیج از خرید package = await _context.Packages .FirstOrDefaultAsync(p => p.Id == packagePurchase.PackageId && !p.IsDeleted, cancellationToken) ?? throw new NotFoundException("پکیج یافت نشد"); // بررسی موجودی با مبلغ پکیج واقعی if (wallet.Balance < package.Price) { throw new BadRequestException( $"برای فعالسازی باشگاه مشتریان باید حداقل {package.Price:N0} تومان موجودی اصلی داشته باشید" ); } } else { _logger.LogInformation( "Force activation by admin for UserId: {UserId} — skipping order/wallet validation", request.UserId ); // در فعال‌سازی اجباری ادمین، اگر روش خرید تنظیم نشده، دستی در نظر بگیر if (user.PackagePurchaseMethod == PackagePurchaseMethod.None) { user.PackagePurchaseMethod = PackagePurchaseMethod.Manual; _context.Users.Update(user); await _context.SaveChangesAsync(cancellationToken); } // بارگذاری پکیج — اگر 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. بررسی عضویت فعلی var existingMembership = await _context.ClubMemberships .FirstOrDefaultAsync(c => c.UserId == user.Id, cancellationToken); // 6.1. دریافت مبلغ هدیه و هزینه فعالسازی از پکیج long giftValue = package.ActivationFee; long activationFeeValue = package.ActivationFee; _logger.LogInformation( "Using Package {PackageId} ({PackageName}): GiftValue={GiftValue}, ActivationFee={ActivationFee}, Price={Price}", package.Id, package.Title, giftValue, activationFeeValue, package.Price ); ClubMembership entity; bool isNewMembership = existingMembership == null; var activationDate = DateTime.Now; if (isNewMembership) { // ایجاد عضویت جدید — IsActive = false تا زمان امضای قرارداد باشگاه entity = new ClubMembership { UserId = user.Id, IsActive = false, ActivatedAt = activationDate, FirstActivationDate = activationDate, FirstPackageId = package.Id, LastActivationDate = activationDate, LastPackageId = package.Id, InitialContribution = activationFeeValue, GiftValue = giftValue, TotalEarned = 0, PurchaseMethod = user.PackagePurchaseMethod }; _context.ClubMemberships.Add(entity); _logger.LogInformation( "Created new club membership for UserId {UserId} via {Method}, GiftValue: {GiftValue}", user.Id, user.PackagePurchaseMethod, giftValue ); } else { // بررسی آیا چرخه فعال دارد — اگر بله، واقعاً فعال است و نیازی به کار نیست var hasCurrentCycle = await _context.ClubMembershipCycles .AnyAsync(c => c.UserId == user.Id && c.IsCurrentCycle, cancellationToken); // اگر چرخه فعال دارد و درخواست Force نیست — احتمال اجرای تکراری همان خرید // در حالت ForceActivation (مثل پرداخت دستی)، باید چرخه جدید ساخته شود تا پکیج جدید ثبت شود. if (hasCurrentCycle && !request.ForceActivation) { _logger.LogInformation( "User {UserId} already has a running cycle — skipping duplicate activation", user.Id ); return true; } // فعال‌سازی مجدد یا خرید مجدد (Q19/Q20) — FirstActivation حفظ میشه (Q21), Last بروزرسانی میشه // مقدار IsActive قبلی حفظ میشه — اگر قبلاً فعال بوده، فعال بمونه entity = existingMembership; entity.LastActivationDate = activationDate; entity.LastPackageId = package.Id; entity.PurchaseMethod = user.PackagePurchaseMethod; _context.ClubMemberships.Update(entity); _logger.LogInformation( "Re-activating club membership for UserId {UserId} (re-purchase scenario, IsActive was {WasActive})", user.Id, existingMembership.IsActive ); } await _context.SaveChangesAsync(cancellationToken); // 6.5. ایجاد ClubMembershipCycle — هر خرید پکیج یک دور جدید var previousCycles = await _context.ClubMembershipCycles .Where(c => c.UserId == user.Id && c.IsCurrentCycle) .ToListAsync(cancellationToken); foreach (var prevCycle in previousCycles) { prevCycle.IsCurrentCycle = false; ClubMembershipCycleHistoryWriter.WriteCycleClosed(_context, prevCycle, reason: "شروع دوره جدید"); } var maxCycleNumber = await _context.ClubMembershipCycles .Where(c => c.ClubMembershipId == entity.Id) .MaxAsync(c => (int?)c.CycleNumber, cancellationToken) ?? 0; var newCycle = new ClubMembershipCycle { UserId = user.Id, ClubMembershipId = entity.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); ClubMembershipCycleHistoryWriter.WriteCycleStarted(_context, newCycle, reason: "فعال‌سازی عضویت باشگاه"); await _context.SaveChangesAsync(cancellationToken); _logger.LogInformation( "Created ClubMembershipCycle #{CycleNumber} for UserId {UserId}, MembershipId {MembershipId}", newCycle.CycleNumber, user.Id, entity.Id ); // 7. ثبت تاریخچه var history = new ClubMembershipHistory { ClubMembershipId = entity.Id, UserId = entity.UserId, OldIsActive = !isNewMembership && !existingMembership!.IsActive, NewIsActive = true, Action = ClubMembershipAction.Activated, Reason = isNewMembership ? $"Initial activation via {user.PackagePurchaseMethod}" : $"Reactivated via {user.PackagePurchaseMethod}", PerformedBy = _currentUser.GetPerformedBy() }; _context.ClubMembershipHistories.Add(history); await _context.SaveChangesAsync(cancellationToken); // ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری // عضو جدید (isNewMembership=true): Pool توسط AcceptClubMembershipContract شارژ می‌شود // چون کاربر قبل از امضای قرارداد IsActive=false است و قرارداد فقط یک‌بار امضا می‌شود. // خرید مجدد (isNewMembership=false): قرارداد مجدد امضا نمی‌شود — Pool همین‌جا شارژ می‌شود. if (!isNewMembership) { var currentWeekDefinitionId = GetCurrentWeekDefinitionId(); var weeklyPool = await _context.WeeklyCommissionPools .FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId && p.PackageId == package.Id, cancellationToken); 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 { _logger.LogInformation( "New member — Pool charge deferred to AcceptClubMembershipContract for 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}", user.Id, entity.Id ); return true; } catch (Exception ex) { _logger.LogError( ex, "Error in ActivateClubMembershipCommand for UserId: {UserId}", request.UserId ); throw; } } /// /// اعمال 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); } /// /// دریافت شناسه تعریف هفته جاری /// private long GetCurrentWeekDefinitionId() { var week = _weekRepository.GetCurrentWeek(); if (week == null) { throw new InvalidOperationException("هفته جاری در سیستم تعریف نشده است"); } return week.Id; } }