using CMSMicroservice.Application.Common.Exceptions; using CMSMicroservice.Application.Common.Interfaces; 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 Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.AcceptClubMembershipContract; /// /// Handler برای پذیرش قرارداد باشگاه مشتریان /// این handler: /// 1. کد OTP را تایید می‌کند /// 2. قرارداد را در جدول UserContract ثبت می‌کند /// 3. باشگاه مشتری را فعال می‌کند (IsActive = true) /// 4. مبلغ را به Pool هفته جاری اضافه می‌کند /// 5. ویژگی‌های باشگاه را به کاربر اعطا می‌کند /// public class AcceptClubMembershipContractCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IConfiguration _cfg; private readonly IHashService _hashService; private readonly ICurrentUserService _currentUser; private readonly IWeekDefinitionRepository _weekRepository; private readonly ILogger _logger; private const int MaxAttempts = 5; private const string OtpPurpose = "signClubContract"; public AcceptClubMembershipContractCommandHandler( IApplicationDbContext context, IConfiguration cfg, IHashService hashService, ICurrentUserService currentUser, IWeekDefinitionRepository weekRepository, ILogger logger) { _context = context; _cfg = cfg; _hashService = hashService; _currentUser = currentUser; _weekRepository = weekRepository; _logger = logger; } public async Task Handle( AcceptClubMembershipContractCommand request, CancellationToken cancellationToken) { // خواندن UserId از JWT (امن‌تر از دریافت از کلاینت) if (!long.TryParse(_currentUser.UserId, out var userId)) return new AcceptClubMembershipContractResponseDto { Success = false, Message = "کاربر احراز هویت نشده است" }; _logger.LogInformation( "Processing club membership contract for UserId: {UserId}", userId ); // 1. دریافت کاربر var user = await _context.Users .Include(u => u.ClubMembership) .FirstOrDefaultAsync(u => u.Id == userId, cancellationToken); if (user == null) { _logger.LogWarning("User not found: {UserId}", userId); throw new NotFoundException(nameof(User), userId); } // 2. بررسی خرید پکیج if (user.PackagePurchaseMethod == PackagePurchaseMethod.None) { return new AcceptClubMembershipContractResponseDto { Success = false, Message = "برای امضای قرارداد باشگاه مشتریان ابتدا باید پکیج پایه را خریداری کنید" }; } // 3. بررسی عدم فعال بودن قبلی باشگاه if (user.ClubMembership?.IsActive == true) { return new AcceptClubMembershipContractResponseDto { Success = false, Message = "شما قبلاً عضو باشگاه مشتریان شده‌اید" }; } // 4. تایید OTP var otpResult = await VerifyOtpAsync(user.Mobile, request.OtpCode, cancellationToken); if (!otpResult.Success) { return new AcceptClubMembershipContractResponseDto { Success = false, Message = otpResult.Message }; } // 5. ثبت قرارداد در جدول UserContract var contract = await _context.Contracts .FirstOrDefaultAsync(c => c.Type == ContractType.ClubMembership, cancellationToken); if (contract == null) { // اگر قرارداد وجود ندارد، یک قرارداد پیش‌فرض ایجاد کنید contract = new Contract { Title = "قرارداد باشگاه مشتریان", Description = "قوانین و مقررات باشگاه مشتریان کارابازار", HtmlContent = request.ContractHtml, Type = ContractType.ClubMembership }; await _context.Contracts.AddAsync(contract, cancellationToken); await _context.SaveChangesAsync(cancellationToken); } var userContract = new UserContract { UserId = user.Id, ContractId = contract.Id, SignGuid = request.SignGuid, SignedPdfFile = request.ContractHtml }; await _context.UserContracts.AddAsync(userContract, cancellationToken); // 6. بارگذاری پکیج از آخرین خرید کاربر یا پکیج پایه var latestPurchase = await _context.UserPackagePurchases .Where(p => p.UserId == userId) .OrderByDescending(p => p.PurchasedAt) .FirstOrDefaultAsync(cancellationToken); var package = latestPurchase != null ? await _context.Packages.FirstOrDefaultAsync(p => p.Id == latestPurchase.PackageId && !p.IsDeleted, cancellationToken) : await _context.Packages.FirstOrDefaultAsync(p => p.IsBasePackage && !p.IsDeleted, cancellationToken); if (package == null) throw new NotFoundException("پکیج یافت نشد"); long giftValue = package.ActivationFee; long activationFeeValue = package.ActivationFee; _logger.LogInformation( "Using Package {PackageId} ({PackageName}): GiftValue={GiftValue}, ActivationFee={ActivationFee}", package.Id, package.Title, giftValue, activationFeeValue ); // 7. فعال‌سازی باشگاه مشتریان ClubMembership clubMembership; bool isNewMembership = user.ClubMembership == null; var activationDate = DateTime.Now; if (isNewMembership) { clubMembership = new ClubMembership { UserId = user.Id, IsActive = true, ActivatedAt = activationDate, FirstActivationDate = activationDate, FirstPackageId = package.Id, LastActivationDate = activationDate, LastPackageId = package.Id, InitialContribution = activationFeeValue, GiftValue = giftValue, TotalEarned = 0, PurchaseMethod = user.PackagePurchaseMethod }; await _context.ClubMemberships.AddAsync(clubMembership, cancellationToken); _logger.LogInformation( "Created new club membership for UserId {UserId} via {Method}, GiftValue: {GiftValue}", user.Id, user.PackagePurchaseMethod, giftValue ); } else { clubMembership = user.ClubMembership!; clubMembership.IsActive = true; clubMembership.ActivatedAt = activationDate; clubMembership.LastActivationDate = activationDate; clubMembership.LastPackageId = package.Id; clubMembership.PurchaseMethod = user.PackagePurchaseMethod; _context.ClubMemberships.Update(clubMembership); _logger.LogInformation( "Reactivated club membership for UserId {UserId}", user.Id ); } await _context.SaveChangesAsync(cancellationToken); // 8. ثبت تاریخچه var history = new ClubMembershipHistory { ClubMembershipId = clubMembership.Id, UserId = clubMembership.UserId, OldIsActive = !isNewMembership && !user.ClubMembership!.IsActive, NewIsActive = true, Action = ClubMembershipAction.Activated, Reason = isNewMembership ? $"Initial activation via contract signing - {user.PackagePurchaseMethod}" : $"Reactivated via contract signing - {user.PackagePurchaseMethod}", PerformedBy = _currentUser.GetPerformedBy() }; _context.ClubMembershipHistories.Add(history); await _context.SaveChangesAsync(cancellationToken); // 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); if (weeklyPool == null) { 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); // 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}", request.UserId, userContract.Id, clubMembership.Id ); return new AcceptClubMembershipContractResponseDto { Success = true, Message = "قرارداد باشگاه مشتریان با موفقیت ثبت شد و عضویت شما فعال گردید", ContractId = userContract.Id }; } /// /// دریافت شناسه تعریف هفته جاری /// private long GetCurrentWeekDefinitionId() { var week = _weekRepository.GetCurrentWeek(); if (week == null) { throw new InvalidOperationException("هفته جاری در سیستم تعریف نشده است"); } 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, CancellationToken cancellationToken) { var normalizedMobile = mobile.NormalizeIranMobile(); var now = DateTime.Now; var otp = await _context.OtpTokens .Where(o => o.Mobile == normalizedMobile && o.Purpose == OtpPurpose && !o.IsUsed && o.ExpiresAt > now) .OrderByDescending(o => o.Created) .FirstOrDefaultAsync(cancellationToken); if (otp == null) { return (false, "کد تایید پیدا نشد یا منقضی شده است"); } if (otp.Attempts >= MaxAttempts) { return (false, "تعداد تلاش‌ها بیش از حد مجاز است. لطفاً کد جدید دریافت کنید"); } otp.Attempts++; var secret = _cfg["Otp:Secret"] ?? throw new InvalidOperationException("Otp:Secret not configured"); if (!_hashService.VerifyHmacSha256Hex(code, otp.CodeHash, secret)) { await _context.SaveChangesAsync(cancellationToken); return (false, "کد تایید نادرست است"); } // کد صحیح است - علامت‌گذاری به عنوان استفاده شده otp.IsUsed = true; await _context.SaveChangesAsync(cancellationToken); return (true, "کد تایید صحیح است"); } }