427d2cce1d
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m45s
- Changed package purchase method from DirectPurchase to Manual in multiple command handlers to reflect new business logic. - Added Manual purchase method to PackagePurchaseMethod enum for clarity. - Introduced new RPC method GetCustomerPackagePurchaseRollup in Protobuf for summarizing customer package purchases. - Updated UserPackagePurchaseProfile and UserPackagePurchaseService to support new rollup functionality. - Bumped Protobuf project version to accommodate new features.
483 lines
21 KiB
C#
483 lines
21 KiB
C#
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<ActivateClubMembershipCommand, bool>
|
|
{
|
|
private readonly IApplicationDbContext _context;
|
|
private readonly ICurrentUserService _currentUser;
|
|
private readonly IWeekDefinitionRepository _weekRepository;
|
|
private readonly ILogger<ActivateClubMembershipCommandHandler> _logger;
|
|
|
|
public ActivateClubMembershipCommandHandler(
|
|
IApplicationDbContext context,
|
|
ICurrentUserService currentUser,
|
|
IWeekDefinitionRepository weekRepository,
|
|
ILogger<ActivateClubMembershipCommandHandler> logger)
|
|
{
|
|
_context = context;
|
|
_currentUser = currentUser;
|
|
_weekRepository = weekRepository;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<bool> 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;
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
private long GetCurrentWeekDefinitionId()
|
|
{
|
|
var week = _weekRepository.GetCurrentWeek();
|
|
if (week == null)
|
|
{
|
|
throw new InvalidOperationException("هفته جاری در سیستم تعریف نشده است");
|
|
}
|
|
return week.Id;
|
|
}
|
|
}
|