1ac23667e2
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)
394 lines
15 KiB
C#
394 lines
15 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Handler برای پذیرش قرارداد باشگاه مشتریان
|
|
/// این handler:
|
|
/// 1. کد OTP را تایید میکند
|
|
/// 2. قرارداد را در جدول UserContract ثبت میکند
|
|
/// 3. باشگاه مشتری را فعال میکند (IsActive = true)
|
|
/// 4. مبلغ را به Pool هفته جاری اضافه میکند
|
|
/// 5. ویژگیهای باشگاه را به کاربر اعطا میکند
|
|
/// </summary>
|
|
public class AcceptClubMembershipContractCommandHandler
|
|
: IRequestHandler<AcceptClubMembershipContractCommand, AcceptClubMembershipContractResponseDto>
|
|
{
|
|
private readonly IApplicationDbContext _context;
|
|
private readonly IConfiguration _cfg;
|
|
private readonly IHashService _hashService;
|
|
private readonly ICurrentUserService _currentUser;
|
|
private readonly IWeekDefinitionRepository _weekRepository;
|
|
private readonly ILogger<AcceptClubMembershipContractCommandHandler> _logger;
|
|
|
|
private const int MaxAttempts = 5;
|
|
private const string OtpPurpose = "signClubContract";
|
|
|
|
public AcceptClubMembershipContractCommandHandler(
|
|
IApplicationDbContext context,
|
|
IConfiguration cfg,
|
|
IHashService hashService,
|
|
ICurrentUserService currentUser,
|
|
IWeekDefinitionRepository weekRepository,
|
|
ILogger<AcceptClubMembershipContractCommandHandler> logger)
|
|
{
|
|
_context = context;
|
|
_cfg = cfg;
|
|
_hashService = hashService;
|
|
_currentUser = currentUser;
|
|
_weekRepository = weekRepository;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<AcceptClubMembershipContractResponseDto> 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);
|
|
|
|
// 9. اضافه کردن مبلغ به Pool هفته جاری
|
|
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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// دریافت شناسه تعریف هفته جاری
|
|
/// </summary>
|
|
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, "کد تایید صحیح است");
|
|
}
|
|
}
|